@revturbine/sdk 0.11.5 → 0.11.6
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 +1678 -1605
- package/dist/headless.d.ts +130 -17
- package/dist/headless.js +2 -2
- package/dist/headless.js.map +1 -1
- package/dist/index.d.ts +130 -17
- package/dist/index.js +2 -2
- package/dist/index.js.map +1 -1
- package/dist/types/web-sdk/controllers.d.ts +44 -4
- package/dist/types/web-sdk/controllers.d.ts.map +1 -1
- package/dist/types/web-sdk/customer-side.d.ts +78 -8
- package/dist/types/web-sdk/customer-side.d.ts.map +1 -1
- package/dist/types/web-sdk/react/usePlacement.d.ts.map +1 -1
- package/package.json +4 -4
package/CHANGELOG.md
CHANGED
|
@@ -1,1605 +1,1678 @@
|
|
|
1
|
-
# Changelog
|
|
2
|
-
|
|
3
|
-
Release notes and breaking changes for the RevTurbine SDKs. `@revturbine/sdk` (npm), `revturbine`
|
|
4
|
-
(PyPI) and `revturbine` (crates.io) are **version-locked** — they always ship the
|
|
5
|
-
same version number, so a version here applies to all three unless an entry says
|
|
6
|
-
otherwise.
|
|
7
|
-
|
|
8
|
-
Verified for `0.8.1`: npm `0.8.1`, PyPI `0.8.1`, crates.io `0.8.1` (2026-09-14).
|
|
9
|
-
|
|
10
|
-
Every breaking change is recorded in the same shape, because the thing that cost
|
|
11
|
-
an integration the most time was not the change itself but not being able to
|
|
12
|
-
answer *"which version broke this, and which version made it loud?"*:
|
|
13
|
-
|
|
14
|
-
| field | what it answers |
|
|
15
|
-
|---|---|
|
|
16
|
-
| **What changed** | the old shape and the new one, concretely |
|
|
17
|
-
| **Landed in** | the version that introduced the change |
|
|
18
|
-
| **Fail-closed in** | the version where the old shape stopped being silently tolerated. Often the same version; when it is **not**, the gap is where integrations break quietly. |
|
|
19
|
-
| **Proving test** | the test in this repo that would fail if the behaviour regressed |
|
|
20
|
-
|
|
21
|
-
A change whose "Fail-closed in" is later than its "Landed in" had a window where
|
|
22
|
-
the old shape was accepted and did nothing. Those windows are the expensive ones.
|
|
23
|
-
|
|
24
|
-
> **What this file is not yet.** A CI guard fails any PR that changes a
|
|
25
|
-
> `@public`-tagged export signature without touching this file. That tagging
|
|
26
|
-
> was thin (4 of ~73 methods) until BL-0005 (2026-09-22, no release — TSDoc
|
|
27
|
-
> and snapshot only) widened it to **34** of `RevTurbineCustomerSdk`'s **67**
|
|
28
|
-
> non-private methods, plus 5 provider/hook entry points
|
|
29
|
-
> (`initRevTurbine`, `RevTurbineProvider`, `useCan`, `useEntitlement`,
|
|
30
|
-
> `useGatedAction`) — every method a customer-facing guide, skill, the
|
|
31
|
-
> starter, or a demo app calls directly. The remaining ~33 methods are
|
|
32
|
-
> internal-only paths (called only by the SDK's own placement components,
|
|
33
|
-
> React bindings, or other internal packages) and stay untagged, so a
|
|
34
|
-
> breaking change to one of *those* still will not be caught automatically —
|
|
35
|
-
> it depends on whoever writes the PR. Entries here are reliable; the absence
|
|
36
|
-
> of an entry for a currently-untagged method is not yet proof that nothing
|
|
37
|
-
> changed.
|
|
38
|
-
|
|
39
|
-
> **Note on 0.x.** These packages are pre-1.0, so breaking changes ship in the
|
|
40
|
-
> **minor** position (`0.6.0` → `0.7.0`), not the major. `npm`'s caret on a `0.x`
|
|
41
|
-
> range does **not** span minors, so `^0.7.0` will not silently pull `0.8.0`.
|
|
42
|
-
|
|
43
|
-
Built-in decision reason values have a separate reviewed
|
|
44
|
-
[compatibility baseline](docs/reason-code-contract.md). Live fixture checks
|
|
45
|
-
protect those values independently of `@public` method tags; baseline changes
|
|
46
|
-
also require a changelog entry.
|
|
47
|
-
|
|
48
|
-
---
|
|
49
|
-
|
|
50
|
-
## 0.11.
|
|
51
|
-
|
|
52
|
-
### `
|
|
53
|
-
|
|
54
|
-
**What changed.**
|
|
55
|
-
|
|
56
|
-
(`
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
`
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
`
|
|
126
|
-
|
|
127
|
-
**
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
`
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
**
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
|
|
190
|
-
|
|
191
|
-
|
|
192
|
-
|
|
193
|
-
|
|
194
|
-
|
|
195
|
-
|
|
196
|
-
|
|
197
|
-
`
|
|
198
|
-
|
|
199
|
-
|
|
200
|
-
|
|
201
|
-
|
|
202
|
-
|
|
203
|
-
|
|
204
|
-
|
|
205
|
-
|
|
206
|
-
|
|
207
|
-
|
|
208
|
-
|
|
209
|
-
|
|
210
|
-
`
|
|
211
|
-
|
|
212
|
-
|
|
213
|
-
|
|
214
|
-
|
|
215
|
-
|
|
216
|
-
|
|
217
|
-
|
|
218
|
-
|
|
219
|
-
|
|
220
|
-
|
|
221
|
-
|
|
222
|
-
|
|
223
|
-
|
|
224
|
-
|
|
225
|
-
|
|
226
|
-
|
|
227
|
-
|
|
228
|
-
|
|
229
|
-
|
|
230
|
-
`
|
|
231
|
-
|
|
232
|
-
|
|
233
|
-
|
|
234
|
-
|
|
235
|
-
|
|
236
|
-
|
|
237
|
-
|
|
238
|
-
|
|
239
|
-
|
|
240
|
-
|
|
241
|
-
|
|
242
|
-
|
|
243
|
-
|
|
244
|
-
|
|
245
|
-
|
|
246
|
-
|
|
247
|
-
|
|
248
|
-
|
|
249
|
-
|
|
250
|
-
|
|
251
|
-
|
|
252
|
-
|
|
253
|
-
|
|
254
|
-
|
|
255
|
-
|
|
256
|
-
|
|
257
|
-
|
|
258
|
-
|
|
259
|
-
|
|
260
|
-
|
|
261
|
-
|
|
262
|
-
|
|
263
|
-
|
|
264
|
-
|
|
265
|
-
|
|
266
|
-
|
|
267
|
-
|
|
268
|
-
|
|
269
|
-
|
|
270
|
-
|
|
271
|
-
|
|
272
|
-
|
|
273
|
-
|
|
274
|
-
|
|
275
|
-
|
|
276
|
-
|
|
277
|
-
|
|
278
|
-
|
|
279
|
-
Playbook.
|
|
280
|
-
|
|
281
|
-
|
|
282
|
-
|
|
283
|
-
|
|
284
|
-
|
|
285
|
-
|
|
286
|
-
|
|
287
|
-
|
|
288
|
-
|
|
289
|
-
|
|
290
|
-
|
|
291
|
-
|
|
292
|
-
**
|
|
293
|
-
|
|
294
|
-
|
|
295
|
-
|
|
296
|
-
|
|
297
|
-
|
|
298
|
-
|
|
299
|
-
|
|
300
|
-
|
|
301
|
-
**
|
|
302
|
-
|
|
303
|
-
|
|
304
|
-
|
|
305
|
-
|
|
306
|
-
|
|
307
|
-
|
|
308
|
-
|
|
309
|
-
|
|
310
|
-
|
|
311
|
-
|
|
312
|
-
|
|
313
|
-
|
|
314
|
-
|
|
315
|
-
|
|
316
|
-
|
|
317
|
-
|
|
318
|
-
|
|
319
|
-
|
|
320
|
-
|
|
321
|
-
|
|
322
|
-
|
|
323
|
-
|
|
324
|
-
|
|
325
|
-
|
|
326
|
-
|
|
327
|
-
|
|
328
|
-
|
|
329
|
-
|
|
330
|
-
|
|
331
|
-
|
|
332
|
-
|
|
333
|
-
|
|
334
|
-
|
|
335
|
-
|
|
336
|
-
|
|
337
|
-
|
|
338
|
-
**
|
|
339
|
-
|
|
340
|
-
|
|
341
|
-
|
|
342
|
-
|
|
343
|
-
|
|
344
|
-
|
|
345
|
-
|
|
346
|
-
|
|
347
|
-
|
|
348
|
-
|
|
349
|
-
|
|
350
|
-
|
|
351
|
-
|
|
352
|
-
|
|
353
|
-
|
|
354
|
-
|
|
355
|
-
|
|
356
|
-
|
|
357
|
-
|
|
|
358
|
-
|
|
359
|
-
|
|
360
|
-
`
|
|
361
|
-
`
|
|
362
|
-
`
|
|
363
|
-
`
|
|
364
|
-
|
|
365
|
-
|
|
366
|
-
|
|
367
|
-
|
|
368
|
-
|
|
369
|
-
|
|
370
|
-
|
|
371
|
-
|
|
372
|
-
|
|
373
|
-
|
|
374
|
-
|
|
375
|
-
|
|
376
|
-
|
|
377
|
-
|
|
378
|
-
|
|
379
|
-
|
|
380
|
-
`@
|
|
381
|
-
|
|
382
|
-
|
|
383
|
-
|
|
384
|
-
|
|
385
|
-
|
|
386
|
-
|
|
387
|
-
|
|
388
|
-
|
|
389
|
-
|
|
390
|
-
|
|
391
|
-
|
|
392
|
-
|
|
393
|
-
|
|
394
|
-
|
|
395
|
-
|
|
396
|
-
|
|
397
|
-
|
|
398
|
-
`
|
|
399
|
-
|
|
400
|
-
|
|
401
|
-
|
|
402
|
-
|
|
403
|
-
|
|
404
|
-
|
|
405
|
-
|
|
406
|
-
|
|
407
|
-
|
|
408
|
-
|
|
409
|
-
|
|
410
|
-
|
|
411
|
-
|
|
412
|
-
|
|
413
|
-
|
|
414
|
-
|
|
415
|
-
|
|
416
|
-
|
|
417
|
-
|
|
418
|
-
|
|
419
|
-
|
|
420
|
-
|
|
421
|
-
|
|
422
|
-
|
|
423
|
-
|
|
424
|
-
|
|
425
|
-
|
|
426
|
-
|
|
427
|
-
`
|
|
428
|
-
`
|
|
429
|
-
|
|
430
|
-
|
|
431
|
-
|
|
432
|
-
|
|
433
|
-
|
|
434
|
-
|
|
435
|
-
|
|
436
|
-
|
|
437
|
-
|
|
438
|
-
|
|
439
|
-
|
|
440
|
-
|
|
441
|
-
|
|
442
|
-
|
|
443
|
-
|
|
444
|
-
|
|
445
|
-
|
|
446
|
-
|
|
447
|
-
|
|
448
|
-
|
|
449
|
-
|
|
450
|
-
|
|
451
|
-
|
|
452
|
-
|
|
453
|
-
|
|
454
|
-
|
|
455
|
-
|
|
456
|
-
|
|
457
|
-
|
|
458
|
-
|
|
459
|
-
|
|
460
|
-
|
|
461
|
-
|
|
462
|
-
|
|
463
|
-
|
|
464
|
-
|
|
465
|
-
|
|
466
|
-
|
|
467
|
-
|
|
468
|
-
|
|
469
|
-
|
|
470
|
-
`
|
|
471
|
-
|
|
472
|
-
|
|
473
|
-
|
|
474
|
-
|
|
475
|
-
|
|
476
|
-
|
|
477
|
-
|
|
478
|
-
|
|
479
|
-
|
|
480
|
-
|
|
481
|
-
|
|
482
|
-
|
|
483
|
-
|
|
484
|
-
|
|
485
|
-
|
|
486
|
-
|
|
487
|
-
|
|
488
|
-
|
|
489
|
-
|
|
490
|
-
|
|
491
|
-
`
|
|
492
|
-
|
|
493
|
-
|
|
494
|
-
|
|
495
|
-
|
|
496
|
-
|
|
497
|
-
|
|
498
|
-
|
|
499
|
-
|
|
500
|
-
|
|
501
|
-
|
|
502
|
-
|
|
503
|
-
|
|
504
|
-
|
|
505
|
-
|
|
506
|
-
|
|
507
|
-
|
|
508
|
-
`
|
|
509
|
-
|
|
510
|
-
|
|
511
|
-
|
|
512
|
-
|
|
513
|
-
|
|
514
|
-
|
|
515
|
-
|
|
516
|
-
|
|
517
|
-
|
|
518
|
-
|
|
519
|
-
|
|
520
|
-
|
|
521
|
-
|
|
522
|
-
|
|
523
|
-
|
|
524
|
-
|
|
525
|
-
|
|
526
|
-
|
|
527
|
-
|
|
528
|
-
|
|
529
|
-
|
|
530
|
-
|
|
531
|
-
|
|
532
|
-
|
|
533
|
-
|
|
534
|
-
|
|
535
|
-
|
|
536
|
-
|
|
537
|
-
|
|
538
|
-
|
|
539
|
-
|
|
540
|
-
|
|
541
|
-
|
|
542
|
-
|
|
543
|
-
|
|
544
|
-
|
|
545
|
-
|
|
546
|
-
|
|
547
|
-
|
|
548
|
-
|
|
549
|
-
|
|
550
|
-
|
|
551
|
-
|
|
552
|
-
|
|
553
|
-
|
|
554
|
-
|
|
555
|
-
|
|
556
|
-
|
|
557
|
-
|
|
558
|
-
|
|
559
|
-
|
|
560
|
-
|
|
561
|
-
|
|
562
|
-
|
|
563
|
-
|
|
564
|
-
`
|
|
565
|
-
`
|
|
566
|
-
|
|
567
|
-
`
|
|
568
|
-
|
|
569
|
-
|
|
570
|
-
|
|
571
|
-
|
|
572
|
-
|
|
573
|
-
|
|
574
|
-
|
|
575
|
-
|
|
576
|
-
|
|
577
|
-
|
|
578
|
-
|
|
579
|
-
|
|
580
|
-
|
|
581
|
-
|
|
582
|
-
|
|
583
|
-
`
|
|
584
|
-
|
|
585
|
-
|
|
586
|
-
|
|
587
|
-
|
|
588
|
-
|
|
589
|
-
|
|
590
|
-
|
|
591
|
-
`
|
|
592
|
-
|
|
593
|
-
|
|
594
|
-
|
|
595
|
-
|
|
596
|
-
|
|
597
|
-
|
|
598
|
-
|
|
599
|
-
|
|
600
|
-
|
|
601
|
-
|
|
602
|
-
|
|
603
|
-
|
|
604
|
-
|
|
605
|
-
|
|
606
|
-
|
|
607
|
-
|
|
608
|
-
|
|
609
|
-
|
|
610
|
-
|
|
611
|
-
|
|
612
|
-
|
|
613
|
-
|
|
614
|
-
|
|
615
|
-
|
|
616
|
-
|
|
617
|
-
|
|
618
|
-
|
|
619
|
-
|
|
620
|
-
|
|
621
|
-
|
|
622
|
-
|
|
623
|
-
|
|
624
|
-
|
|
625
|
-
|
|
626
|
-
|
|
627
|
-
|
|
628
|
-
|
|
629
|
-
|
|
630
|
-
|
|
631
|
-
|
|
632
|
-
|
|
633
|
-
|
|
634
|
-
|
|
635
|
-
|
|
636
|
-
|
|
637
|
-
|
|
638
|
-
|
|
639
|
-
|
|
640
|
-
|
|
641
|
-
|
|
642
|
-
`
|
|
643
|
-
|
|
644
|
-
|
|
645
|
-
|
|
646
|
-
|
|
647
|
-
|
|
648
|
-
|
|
649
|
-
|
|
650
|
-
|
|
651
|
-
|
|
652
|
-
|
|
653
|
-
|
|
654
|
-
|
|
655
|
-
|
|
656
|
-
|
|
657
|
-
|
|
658
|
-
|
|
659
|
-
|
|
660
|
-
|
|
661
|
-
|
|
662
|
-
|
|
663
|
-
`
|
|
664
|
-
|
|
665
|
-
|
|
666
|
-
|
|
667
|
-
|
|
668
|
-
|
|
669
|
-
|
|
670
|
-
|
|
671
|
-
|
|
672
|
-
|
|
673
|
-
|
|
674
|
-
|
|
675
|
-
|
|
676
|
-
|
|
677
|
-
|
|
678
|
-
|
|
679
|
-
|
|
680
|
-
|
|
681
|
-
|
|
682
|
-
|
|
683
|
-
|
|
684
|
-
|
|
685
|
-
|
|
686
|
-
(`
|
|
687
|
-
|
|
688
|
-
|
|
689
|
-
**
|
|
690
|
-
`
|
|
691
|
-
|
|
692
|
-
`
|
|
693
|
-
`
|
|
694
|
-
|
|
695
|
-
|
|
696
|
-
|
|
697
|
-
|
|
698
|
-
|
|
699
|
-
|
|
700
|
-
|
|
701
|
-
|
|
702
|
-
|
|
703
|
-
|
|
704
|
-
|
|
705
|
-
|
|
706
|
-
|
|
707
|
-
|
|
708
|
-
|
|
709
|
-
|
|
710
|
-
|
|
711
|
-
|
|
712
|
-
|
|
713
|
-
|
|
714
|
-
|
|
715
|
-
`
|
|
716
|
-
|
|
717
|
-
-
|
|
718
|
-
|
|
719
|
-
|
|
720
|
-
|
|
721
|
-
|
|
722
|
-
|
|
723
|
-
|
|
724
|
-
|
|
725
|
-
|
|
726
|
-
|
|
727
|
-
|
|
728
|
-
|
|
729
|
-
|
|
730
|
-
|
|
731
|
-
|
|
732
|
-
|
|
733
|
-
|
|
734
|
-
|
|
735
|
-
|
|
736
|
-
|
|
737
|
-
|
|
738
|
-
|
|
739
|
-
|
|
740
|
-
|
|
741
|
-
|
|
742
|
-
|
|
743
|
-
|
|
744
|
-
|
|
745
|
-
|
|
746
|
-
|
|
747
|
-
|
|
748
|
-
|
|
749
|
-
|
|
750
|
-
|
|
751
|
-
|
|
752
|
-
|
|
753
|
-
|
|
754
|
-
|
|
755
|
-
|
|
756
|
-
**
|
|
757
|
-
|
|
758
|
-
|
|
759
|
-
|
|
760
|
-
|
|
761
|
-
|
|
762
|
-
|
|
763
|
-
|
|
764
|
-
|
|
765
|
-
|
|
766
|
-
|
|
767
|
-
|
|
768
|
-
|
|
769
|
-
|
|
770
|
-
|
|
771
|
-
|
|
772
|
-
|
|
773
|
-
|
|
774
|
-
**
|
|
775
|
-
|
|
776
|
-
|
|
777
|
-
|
|
778
|
-
|
|
779
|
-
|
|
780
|
-
|
|
781
|
-
|
|
782
|
-
|
|
783
|
-
|
|
784
|
-
|
|
785
|
-
|
|
786
|
-
|
|
787
|
-
|
|
788
|
-
|
|
789
|
-
|
|
790
|
-
|
|
791
|
-
|
|
792
|
-
|
|
793
|
-
|
|
794
|
-
|
|
795
|
-
|
|
796
|
-
|
|
797
|
-
|
|
798
|
-
|
|
799
|
-
|
|
800
|
-
|
|
801
|
-
|
|
802
|
-
|
|
803
|
-
|
|
804
|
-
|
|
805
|
-
|
|
806
|
-
|
|
807
|
-
|
|
808
|
-
|
|
809
|
-
|
|
810
|
-
|
|
811
|
-
|
|
812
|
-
|
|
813
|
-
|
|
814
|
-
|
|
815
|
-
|
|
816
|
-
|
|
817
|
-
|
|
818
|
-
|
|
819
|
-
|
|
820
|
-
`
|
|
821
|
-
|
|
822
|
-
|
|
823
|
-
|
|
824
|
-
|
|
825
|
-
|
|
826
|
-
|
|
827
|
-
|
|
828
|
-
|
|
829
|
-
|
|
830
|
-
|
|
831
|
-
|
|
832
|
-
|
|
833
|
-
|
|
834
|
-
|
|
835
|
-
|
|
836
|
-
|
|
837
|
-
|
|
838
|
-
|
|
839
|
-
|
|
840
|
-
|
|
841
|
-
|
|
842
|
-
|
|
843
|
-
|
|
844
|
-
|
|
845
|
-
|
|
846
|
-
|
|
847
|
-
|
|
848
|
-
|
|
849
|
-
|
|
850
|
-
|
|
851
|
-
|
|
852
|
-
|
|
853
|
-
|
|
854
|
-
|
|
855
|
-
|
|
856
|
-
|
|
857
|
-
|
|
858
|
-
|
|
859
|
-
|
|
860
|
-
|
|
861
|
-
|
|
862
|
-
|
|
863
|
-
|
|
864
|
-
|
|
865
|
-
|
|
866
|
-
|
|
867
|
-
**
|
|
868
|
-
|
|
869
|
-
|
|
870
|
-
|
|
871
|
-
|
|
872
|
-
|
|
873
|
-
|
|
874
|
-
|
|
875
|
-
|
|
876
|
-
|
|
877
|
-
|
|
878
|
-
|
|
879
|
-
|
|
880
|
-
|
|
881
|
-
|
|
882
|
-
|
|
883
|
-
|
|
884
|
-
|
|
885
|
-
`
|
|
886
|
-
|
|
887
|
-
|
|
888
|
-
|
|
889
|
-
|
|
890
|
-
`
|
|
891
|
-
|
|
892
|
-
|
|
893
|
-
|
|
894
|
-
|
|
895
|
-
|
|
896
|
-
|
|
897
|
-
|
|
898
|
-
|
|
899
|
-
|
|
900
|
-
|
|
901
|
-
|
|
902
|
-
|
|
903
|
-
|
|
904
|
-
|
|
905
|
-
|
|
906
|
-
|
|
907
|
-
|
|
908
|
-
|
|
909
|
-
|
|
910
|
-
|
|
911
|
-
|
|
912
|
-
|
|
913
|
-
|
|
914
|
-
|
|
915
|
-
|
|
916
|
-
|
|
917
|
-
|
|
918
|
-
|
|
919
|
-
|
|
920
|
-
|
|
921
|
-
|
|
922
|
-
|
|
923
|
-
|
|
924
|
-
|
|
925
|
-
|
|
926
|
-
|
|
927
|
-
|
|
928
|
-
|
|
929
|
-
|
|
930
|
-
|
|
931
|
-
|
|
932
|
-
|
|
933
|
-
|
|
934
|
-
|
|
935
|
-
|
|
936
|
-
|
|
937
|
-
|
|
938
|
-
|
|
939
|
-
|
|
940
|
-
|
|
941
|
-
|
|
942
|
-
|
|
943
|
-
|
|
944
|
-
|
|
945
|
-
|
|
946
|
-
|
|
947
|
-
|
|
948
|
-
|
|
949
|
-
|
|
950
|
-
|
|
951
|
-
|
|
952
|
-
|
|
953
|
-
|
|
954
|
-
|
|
955
|
-
|
|
956
|
-
|
|
957
|
-
`
|
|
958
|
-
`
|
|
959
|
-
|
|
960
|
-
|
|
961
|
-
|
|
962
|
-
|
|
963
|
-
|
|
964
|
-
|
|
965
|
-
|
|
966
|
-
|
|
967
|
-
|
|
968
|
-
|
|
969
|
-
|
|
970
|
-
|
|
971
|
-
|
|
972
|
-
|
|
973
|
-
|
|
974
|
-
|
|
975
|
-
|
|
976
|
-
|
|
977
|
-
|
|
978
|
-
|
|
979
|
-
|
|
980
|
-
|
|
981
|
-
in `
|
|
982
|
-
|
|
983
|
-
|
|
984
|
-
|
|
985
|
-
|
|
986
|
-
|
|
987
|
-
-
|
|
988
|
-
|
|
989
|
-
|
|
990
|
-
|
|
991
|
-
|
|
992
|
-
|
|
993
|
-
|
|
994
|
-
|
|
995
|
-
|
|
996
|
-
|
|
997
|
-
|
|
998
|
-
|
|
999
|
-
|
|
1000
|
-
|
|
1001
|
-
|
|
1002
|
-
|
|
1003
|
-
|
|
1004
|
-
|
|
1005
|
-
|
|
1006
|
-
|
|
1007
|
-
|
|
1008
|
-
|
|
1009
|
-
|
|
1010
|
-
|
|
1011
|
-
|
|
1012
|
-
|
|
1013
|
-
|
|
1014
|
-
|
|
1015
|
-
|
|
1016
|
-
|
|
1017
|
-
|
|
1018
|
-
|
|
1019
|
-
|
|
1020
|
-
-
|
|
1021
|
-
|
|
1022
|
-
|
|
1023
|
-
|
|
1024
|
-
|
|
1025
|
-
|
|
1026
|
-
|
|
1027
|
-
|
|
1028
|
-
|
|
1029
|
-
|
|
1030
|
-
|
|
1031
|
-
|
|
1032
|
-
|
|
1033
|
-
|
|
1034
|
-
|
|
1035
|
-
|
|
1036
|
-
|
|
1037
|
-
|
|
1038
|
-
|
|
1039
|
-
|
|
1040
|
-
|
|
1041
|
-
|
|
1042
|
-
|
|
1043
|
-
|
|
1044
|
-
|
|
1045
|
-
|
|
1046
|
-
|
|
1047
|
-
|
|
1048
|
-
|
|
1049
|
-
-
|
|
1050
|
-
|
|
1051
|
-
|
|
1052
|
-
|
|
1053
|
-
|
|
1054
|
-
|
|
1055
|
-
|
|
1056
|
-
|
|
1057
|
-
|
|
1058
|
-
|
|
1059
|
-
|
|
1060
|
-
|
|
1061
|
-
|
|
1062
|
-
|
|
1063
|
-
|
|
1064
|
-
|
|
1065
|
-
|
|
1066
|
-
|
|
1067
|
-
|
|
1068
|
-
|
|
1069
|
-
|
|
1070
|
-
|
|
1071
|
-
|
|
1072
|
-
|
|
1073
|
-
|
|
1074
|
-
|
|
1075
|
-
|
|
1076
|
-
|
|
1077
|
-
|
|
1078
|
-
|
|
1079
|
-
|
|
1080
|
-
|
|
1081
|
-
|
|
1082
|
-
|
|
1083
|
-
|
|
1084
|
-
|
|
1085
|
-
|
|
1086
|
-
|
|
1087
|
-
|
|
1088
|
-
|
|
1089
|
-
|
|
1090
|
-
|
|
1091
|
-
|
|
1092
|
-
|
|
1093
|
-
|
|
1094
|
-
|
|
1095
|
-
|
|
1096
|
-
|
|
1097
|
-
|
|
1098
|
-
-
|
|
1099
|
-
|
|
1100
|
-
|
|
1101
|
-
|
|
1102
|
-
|
|
1103
|
-
|
|
1104
|
-
|
|
1105
|
-
|
|
1106
|
-
`
|
|
1107
|
-
|
|
1108
|
-
-
|
|
1109
|
-
|
|
1110
|
-
|
|
1111
|
-
|
|
1112
|
-
|
|
1113
|
-
|
|
1114
|
-
|
|
1115
|
-
- **
|
|
1116
|
-
|
|
1117
|
-
|
|
1118
|
-
|
|
1119
|
-
|
|
1120
|
-
|
|
1121
|
-
|
|
1122
|
-
|
|
1123
|
-
|
|
1124
|
-
|
|
1125
|
-
|
|
1126
|
-
|
|
1127
|
-
|
|
1128
|
-
|
|
1129
|
-
|
|
1130
|
-
|
|
1131
|
-
|
|
1132
|
-
|
|
1133
|
-
|
|
1134
|
-
|
|
1135
|
-
|
|
1136
|
-
|
|
1137
|
-
|
|
1138
|
-
|
|
1139
|
-
|
|
1140
|
-
|
|
1141
|
-
|
|
1142
|
-
|
|
1143
|
-
|
|
1144
|
-
|
|
1145
|
-
|
|
1146
|
-
|
|
1147
|
-
|
|
1148
|
-
|
|
1149
|
-
|
|
1150
|
-
|
|
1151
|
-
|
|
1152
|
-
|
|
1153
|
-
|
|
1154
|
-
|
|
1155
|
-
|
|
1156
|
-
|
|
1157
|
-
|
|
1158
|
-
|
|
1159
|
-
|
|
1160
|
-
|
|
1161
|
-
|
|
1162
|
-
|
|
1163
|
-
|
|
1164
|
-
|
|
1165
|
-
|
|
1166
|
-
|
|
1167
|
-
|
|
1168
|
-
|
|
1169
|
-
|
|
1170
|
-
|
|
1171
|
-
|
|
1172
|
-
|
|
1173
|
-
|
|
1174
|
-
-
|
|
1175
|
-
|
|
1176
|
-
|
|
1177
|
-
|
|
1178
|
-
|
|
1179
|
-
|
|
1180
|
-
|
|
1181
|
-
|
|
1182
|
-
|
|
1183
|
-
|
|
1184
|
-
|
|
1185
|
-
|
|
1186
|
-
|
|
1187
|
-
|
|
1188
|
-
- **
|
|
1189
|
-
|
|
1190
|
-
|
|
1191
|
-
|
|
1192
|
-
|
|
1193
|
-
|
|
1194
|
-
|
|
1195
|
-
|
|
1196
|
-
|
|
1197
|
-
|
|
1198
|
-
|
|
1199
|
-
|
|
1200
|
-
|
|
1201
|
-
|
|
1202
|
-
|
|
1203
|
-
|
|
1204
|
-
|
|
1205
|
-
|
|
1206
|
-
|
|
1207
|
-
|
|
1208
|
-
|
|
1209
|
-
|
|
1210
|
-
|
|
1211
|
-
|
|
1212
|
-
|
|
1213
|
-
|
|
1214
|
-
|
|
1215
|
-
|
|
1216
|
-
|
|
1217
|
-
|
|
1218
|
-
- **
|
|
1219
|
-
|
|
1220
|
-
|
|
1221
|
-
|
|
1222
|
-
|
|
1223
|
-
|
|
1224
|
-
|
|
1225
|
-
|
|
1226
|
-
|
|
1227
|
-
|
|
1228
|
-
|
|
1229
|
-
|
|
1230
|
-
|
|
1231
|
-
|
|
1232
|
-
|
|
1233
|
-
|
|
1234
|
-
|
|
1235
|
-
the
|
|
1236
|
-
|
|
1237
|
-
|
|
1238
|
-
|
|
1239
|
-
|
|
1240
|
-
|
|
1241
|
-
|
|
1242
|
-
|
|
1243
|
-
|
|
1244
|
-
|
|
1245
|
-
|
|
1246
|
-
|
|
1247
|
-
|
|
1248
|
-
|
|
1249
|
-
|
|
1250
|
-
|
|
1251
|
-
|
|
1252
|
-
|
|
1253
|
-
|
|
1254
|
-
|
|
1255
|
-
|
|
1256
|
-
|
|
1257
|
-
|
|
1258
|
-
|
|
1259
|
-
|
|
1260
|
-
|
|
1261
|
-
- **
|
|
1262
|
-
|
|
1263
|
-
|
|
1264
|
-
|
|
1265
|
-
|
|
1266
|
-
|
|
1267
|
-
|
|
1268
|
-
|
|
1269
|
-
|
|
1270
|
-
|
|
1271
|
-
|
|
1272
|
-
|
|
1273
|
-
|
|
1274
|
-
|
|
1275
|
-
|
|
1276
|
-
|
|
1277
|
-
|
|
1278
|
-
|
|
1279
|
-
|
|
1280
|
-
|
|
1281
|
-
|
|
1282
|
-
the
|
|
1283
|
-
|
|
1284
|
-
|
|
1285
|
-
|
|
1286
|
-
|
|
1287
|
-
|
|
1288
|
-
|
|
1289
|
-
|
|
1290
|
-
|
|
1291
|
-
- **
|
|
1292
|
-
|
|
1293
|
-
|
|
1294
|
-
|
|
1295
|
-
|
|
1296
|
-
`
|
|
1297
|
-
|
|
1298
|
-
|
|
1299
|
-
|
|
1300
|
-
|
|
1301
|
-
|
|
1302
|
-
|
|
1303
|
-
|
|
1304
|
-
|
|
1305
|
-
**
|
|
1306
|
-
|
|
1307
|
-
|
|
1308
|
-
|
|
1309
|
-
|
|
1310
|
-
|
|
1311
|
-
|
|
1312
|
-
|
|
1313
|
-
|
|
1314
|
-
|
|
1315
|
-
|
|
1316
|
-
|
|
1317
|
-
|
|
1318
|
-
|
|
1319
|
-
|
|
1320
|
-
|
|
1321
|
-
-
|
|
1322
|
-
|
|
1323
|
-
|
|
1324
|
-
-
|
|
1325
|
-
|
|
1326
|
-
|
|
1327
|
-
|
|
1328
|
-
|
|
1329
|
-
|
|
1330
|
-
|
|
1331
|
-
|
|
1332
|
-
|
|
1333
|
-
- **
|
|
1334
|
-
- **
|
|
1335
|
-
|
|
1336
|
-
|
|
1337
|
-
|
|
1338
|
-
|
|
1339
|
-
|
|
1340
|
-
|
|
1341
|
-
|
|
1342
|
-
|
|
1343
|
-
|
|
1344
|
-
|
|
1345
|
-
|
|
1346
|
-
|
|
1347
|
-
|
|
1348
|
-
-
|
|
1349
|
-
|
|
1350
|
-
|
|
1351
|
-
|
|
1352
|
-
|
|
1353
|
-
|
|
1354
|
-
|
|
1355
|
-
|
|
1356
|
-
|
|
1357
|
-
|
|
1358
|
-
|
|
1359
|
-
a non-empty
|
|
1360
|
-
|
|
1361
|
-
-
|
|
1362
|
-
|
|
1363
|
-
|
|
1364
|
-
|
|
1365
|
-
|
|
1366
|
-
- **
|
|
1367
|
-
`
|
|
1368
|
-
|
|
1369
|
-
|
|
1370
|
-
|
|
1371
|
-
|
|
1372
|
-
|
|
1373
|
-
|
|
1374
|
-
|
|
1375
|
-
|
|
1376
|
-
|
|
1377
|
-
|
|
1378
|
-
|
|
1379
|
-
|
|
1380
|
-
|
|
1381
|
-
|
|
1382
|
-
|
|
1383
|
-
|
|
1384
|
-
|
|
1385
|
-
|
|
1386
|
-
|
|
1387
|
-
|
|
1388
|
-
|
|
1389
|
-
|
|
1390
|
-
|
|
1391
|
-
|
|
1392
|
-
|
|
1393
|
-
|
|
1394
|
-
|
|
1395
|
-
|
|
1396
|
-
|
|
1397
|
-
|
|
1398
|
-
|
|
1399
|
-
|
|
1400
|
-
|
|
1401
|
-
|
|
1402
|
-
|
|
1403
|
-
|
|
1404
|
-
|
|
1405
|
-
|
|
1406
|
-
- **
|
|
1407
|
-
|
|
1408
|
-
|
|
1409
|
-
|
|
1410
|
-
|
|
1411
|
-
|
|
1412
|
-
|
|
1413
|
-
|
|
1414
|
-
|
|
1415
|
-
|
|
1416
|
-
|
|
1417
|
-
|
|
1418
|
-
|
|
1419
|
-
`
|
|
1420
|
-
|
|
1421
|
-
- **
|
|
1422
|
-
|
|
1423
|
-
-
|
|
1424
|
-
- **
|
|
1425
|
-
|
|
1426
|
-
|
|
1427
|
-
|
|
1428
|
-
- **
|
|
1429
|
-
|
|
1430
|
-
|
|
1431
|
-
|
|
1432
|
-
-
|
|
1433
|
-
|
|
1434
|
-
|
|
1435
|
-
|
|
1436
|
-
|
|
1437
|
-
|
|
1438
|
-
|
|
1439
|
-
- **
|
|
1440
|
-
`
|
|
1441
|
-
|
|
1442
|
-
|
|
1443
|
-
- **
|
|
1444
|
-
- **
|
|
1445
|
-
|
|
1446
|
-
|
|
1447
|
-
|
|
1448
|
-
|
|
1449
|
-
|
|
1450
|
-
|
|
1451
|
-
|
|
1452
|
-
|
|
1453
|
-
|
|
1454
|
-
|
|
1455
|
-
|
|
1456
|
-
|
|
1457
|
-
|
|
1458
|
-
|
|
1459
|
-
|
|
1460
|
-
|
|
1461
|
-
|
|
1462
|
-
|
|
1463
|
-
- **
|
|
1464
|
-
|
|
1465
|
-
|
|
1466
|
-
|
|
1467
|
-
|
|
1468
|
-
|
|
1469
|
-
|
|
1470
|
-
|
|
1471
|
-
|
|
1472
|
-
|
|
1473
|
-
- **
|
|
1474
|
-
-
|
|
1475
|
-
|
|
1476
|
-
|
|
1477
|
-
|
|
1478
|
-
|
|
1479
|
-
|
|
1480
|
-
|
|
1481
|
-
|
|
1482
|
-
|
|
1483
|
-
|
|
1484
|
-
|
|
1485
|
-
|
|
1486
|
-
|
|
1487
|
-
|
|
1488
|
-
|
|
1489
|
-
|
|
1490
|
-
|
|
1491
|
-
|
|
1492
|
-
|
|
1493
|
-
|
|
1494
|
-
|
|
1495
|
-
|
|
1496
|
-
|
|
1497
|
-
|
|
1498
|
-
|
|
1499
|
-
|
|
1500
|
-
|
|
1501
|
-
- **
|
|
1502
|
-
|
|
1503
|
-
|
|
1504
|
-
|
|
1505
|
-
|
|
1506
|
-
|
|
1507
|
-
|
|
1508
|
-
|
|
1509
|
-
|
|
1510
|
-
|
|
1511
|
-
|
|
1512
|
-
|
|
1513
|
-
|
|
1514
|
-
|
|
1515
|
-
- **
|
|
1516
|
-
|
|
1517
|
-
|
|
1518
|
-
|
|
1519
|
-
|
|
1520
|
-
|
|
1521
|
-
|
|
1522
|
-
|
|
1523
|
-
|
|
1524
|
-
|
|
1525
|
-
|
|
1526
|
-
|
|
1527
|
-
|
|
1528
|
-
|
|
1529
|
-
|
|
1530
|
-
|
|
1531
|
-
|
|
1532
|
-
|
|
1533
|
-
|
|
1534
|
-
- **Landed in** — `0.
|
|
1535
|
-
- **Fail-closed in** —
|
|
1536
|
-
- **Proving test** — `web-sdk/
|
|
1537
|
-
|
|
1538
|
-
|
|
1539
|
-
|
|
1540
|
-
|
|
1541
|
-
|
|
1542
|
-
|
|
1543
|
-
|
|
1544
|
-
|
|
1545
|
-
|
|
1546
|
-
|
|
1547
|
-
- **
|
|
1548
|
-
|
|
1549
|
-
|
|
1550
|
-
|
|
1551
|
-
|
|
1552
|
-
|
|
1553
|
-
|
|
1554
|
-
|
|
1555
|
-
|
|
1556
|
-
|
|
1557
|
-
|
|
1558
|
-
|
|
1559
|
-
|
|
1560
|
-
|
|
1561
|
-
|
|
1562
|
-
|
|
1563
|
-
|
|
1564
|
-
|
|
1565
|
-
- **
|
|
1566
|
-
|
|
1567
|
-
|
|
1568
|
-
|
|
1569
|
-
|
|
1570
|
-
|
|
1571
|
-
|
|
1572
|
-
|
|
1573
|
-
|
|
1574
|
-
|
|
1575
|
-
|
|
1576
|
-
`
|
|
1577
|
-
|
|
1578
|
-
|
|
1579
|
-
|
|
1580
|
-
-
|
|
1581
|
-
|
|
1582
|
-
-
|
|
1583
|
-
|
|
1584
|
-
- **
|
|
1585
|
-
|
|
1586
|
-
|
|
1587
|
-
|
|
1588
|
-
- **
|
|
1589
|
-
|
|
1590
|
-
|
|
1591
|
-
- **
|
|
1592
|
-
|
|
1593
|
-
|
|
1594
|
-
|
|
1595
|
-
|
|
1596
|
-
|
|
1597
|
-
|
|
1598
|
-
|
|
1599
|
-
|
|
1600
|
-
|
|
1601
|
-
|
|
1602
|
-
|
|
1603
|
-
|
|
1604
|
-
|
|
1605
|
-
|
|
1
|
+
# Changelog
|
|
2
|
+
|
|
3
|
+
Release notes and breaking changes for the RevTurbine SDKs. `@revturbine/sdk` (npm), `revturbine`
|
|
4
|
+
(PyPI) and `revturbine` (crates.io) are **version-locked** — they always ship the
|
|
5
|
+
same version number, so a version here applies to all three unless an entry says
|
|
6
|
+
otherwise.
|
|
7
|
+
|
|
8
|
+
Verified for `0.8.1`: npm `0.8.1`, PyPI `0.8.1`, crates.io `0.8.1` (2026-09-14).
|
|
9
|
+
|
|
10
|
+
Every breaking change is recorded in the same shape, because the thing that cost
|
|
11
|
+
an integration the most time was not the change itself but not being able to
|
|
12
|
+
answer *"which version broke this, and which version made it loud?"*:
|
|
13
|
+
|
|
14
|
+
| field | what it answers |
|
|
15
|
+
|---|---|
|
|
16
|
+
| **What changed** | the old shape and the new one, concretely |
|
|
17
|
+
| **Landed in** | the version that introduced the change |
|
|
18
|
+
| **Fail-closed in** | the version where the old shape stopped being silently tolerated. Often the same version; when it is **not**, the gap is where integrations break quietly. |
|
|
19
|
+
| **Proving test** | the test in this repo that would fail if the behaviour regressed |
|
|
20
|
+
|
|
21
|
+
A change whose "Fail-closed in" is later than its "Landed in" had a window where
|
|
22
|
+
the old shape was accepted and did nothing. Those windows are the expensive ones.
|
|
23
|
+
|
|
24
|
+
> **What this file is not yet.** A CI guard fails any PR that changes a
|
|
25
|
+
> `@public`-tagged export signature without touching this file. That tagging
|
|
26
|
+
> was thin (4 of ~73 methods) until BL-0005 (2026-09-22, no release — TSDoc
|
|
27
|
+
> and snapshot only) widened it to **34** of `RevTurbineCustomerSdk`'s **67**
|
|
28
|
+
> non-private methods, plus 5 provider/hook entry points
|
|
29
|
+
> (`initRevTurbine`, `RevTurbineProvider`, `useCan`, `useEntitlement`,
|
|
30
|
+
> `useGatedAction`) — every method a customer-facing guide, skill, the
|
|
31
|
+
> starter, or a demo app calls directly. The remaining ~33 methods are
|
|
32
|
+
> internal-only paths (called only by the SDK's own placement components,
|
|
33
|
+
> React bindings, or other internal packages) and stay untagged, so a
|
|
34
|
+
> breaking change to one of *those* still will not be caught automatically —
|
|
35
|
+
> it depends on whoever writes the PR. Entries here are reliable; the absence
|
|
36
|
+
> of an entry for a currently-untagged method is not yet proof that nothing
|
|
37
|
+
> changed.
|
|
38
|
+
|
|
39
|
+
> **Note on 0.x.** These packages are pre-1.0, so breaking changes ship in the
|
|
40
|
+
> **minor** position (`0.6.0` → `0.7.0`), not the major. `npm`'s caret on a `0.x`
|
|
41
|
+
> range does **not** span minors, so `^0.7.0` will not silently pull `0.8.0`.
|
|
42
|
+
|
|
43
|
+
Built-in decision reason values have a separate reviewed
|
|
44
|
+
[compatibility baseline](docs/reason-code-contract.md). Live fixture checks
|
|
45
|
+
protect those values independently of `@public` method tags; baseline changes
|
|
46
|
+
also require a changelog entry.
|
|
47
|
+
|
|
48
|
+
---
|
|
49
|
+
|
|
50
|
+
## 0.11.6
|
|
51
|
+
|
|
52
|
+
### `sdk.convert()` reloads the UserContext, so a mounted slot re-decides (BL-0004)
|
|
53
|
+
|
|
54
|
+
**What changed.** `sdk.convert(outputId)` used to record the conversion and stop
|
|
55
|
+
there. Conversion writes **no** cooldown and **no** permanent retirement — by
|
|
56
|
+
design (Kent's plan 254 D-9 ruling; the `convert` row of the spec's interaction
|
|
57
|
+
table) — so eligibility is a function of the user's CURRENT plan and targeting.
|
|
58
|
+
That left a real gap: a host that called the public convert path while the
|
|
59
|
+
placement was mounted saw the upgrade banner stay on screen until the slot next
|
|
60
|
+
happened to resolve a decision. The documented workaround was a manual
|
|
61
|
+
`refresh()` or the component's `ctaComplete()`. Plan 236 TASK-13 recorded it as
|
|
62
|
+
an open product call rather than asserting it away.
|
|
63
|
+
|
|
64
|
+
Kent ruled on it, 2026-09-25: *"Converting should trigger a reloading of
|
|
65
|
+
UserContext with the new plan/billing state the user has converted to."*
|
|
66
|
+
|
|
67
|
+
So `convert()` now, after recording the interaction:
|
|
68
|
+
|
|
69
|
+
1. **Optimistically** moves the held plan when the converted output's CTA names
|
|
70
|
+
one (`cta_path.plan_handle` — e.g. `open_checkout` with `config.purchase`).
|
|
71
|
+
The Playbook supplies the plan's display name when it knows it.
|
|
72
|
+
2. Notifies user-context subscribers **either way** — a CTA that names no plan
|
|
73
|
+
still re-decides mounted surfaces.
|
|
74
|
+
3. **Then** refreshes from `GET /api/sdk/client-context`, which overlays the
|
|
75
|
+
RevTurbine-authoritative trial / billing-health / plan fields. Best-effort and
|
|
76
|
+
never throwing, exactly as before; a no-op when no client-session minter is
|
|
77
|
+
configured.
|
|
78
|
+
|
|
79
|
+
Optimistic-first because billing truth arrives by Stripe webhook: a
|
|
80
|
+
`client-context` read that races the webhook still reports the OLD plan, and
|
|
81
|
+
waiting for it would leave the upgrade prompt up at the exact moment the user
|
|
82
|
+
paid. For the same reason, while a conversion is pending a server plan **equal to
|
|
83
|
+
the plan converted away from** is dropped (plan fields only — trial and
|
|
84
|
+
billing-health always apply). Any other value clears the pending state and
|
|
85
|
+
applies, so the suppression cannot outlive the lag it covers.
|
|
86
|
+
|
|
87
|
+
Mounted `PlacementController`s re-decide and mounted `EntitlementGate`s re-check
|
|
88
|
+
through `watchUserContext()`. The React bindings attach it for you
|
|
89
|
+
(`usePlacement`, and so every surface slot built on it; `useEntitlement` /
|
|
90
|
+
`useCan` already did), and `SdkSession.placement()` / `.entitlement()` attach it
|
|
91
|
+
for headless consumers. `dispose()` drops it. `PlacementController.ctaComplete()`
|
|
92
|
+
is unchanged.
|
|
93
|
+
|
|
94
|
+
**What did NOT change.** `convert()`'s signature. Conversion still writes no
|
|
95
|
+
suppression and no retirement: back on the old plan, the placement is eligible
|
|
96
|
+
again. The optimistic plan move is a session-local overlay, not a new source of
|
|
97
|
+
plan truth — a host that re-declares `plan_handle` on mount wins, and that is the
|
|
98
|
+
intended precedence.
|
|
99
|
+
|
|
100
|
+
**Landed in.** `0.11.6` (BL-0004; refs BL-0177, BL-0179 for the re-decide
|
|
101
|
+
mechanism this reuses).
|
|
102
|
+
|
|
103
|
+
**Fail-closed in.** `0.11.6` — same version. Nothing was silently tolerated
|
|
104
|
+
before: the old behaviour was a placement that stayed visible, which is why the
|
|
105
|
+
limitation was documented rather than dated.
|
|
106
|
+
|
|
107
|
+
**Proving test.** `web-sdk/convert-reloads-user-context.test.ts` (headless: the
|
|
108
|
+
mounted controller's decision goes invisible, a failed conversion moves nothing,
|
|
109
|
+
a CTA with no plan still notifies, and the stale-server-plan guard both
|
|
110
|
+
suppresses and releases); `web-sdk/react/convert-reloads-user-context.test.tsx`
|
|
111
|
+
(a mounted `usePlacement` banner disappears in place and a mounted `useCan` gate
|
|
112
|
+
flips denied → granted, with no remount); `e2e/journey.spec.ts` BL-0004 legs (a
|
|
113
|
+
real browser: the converted placement leaves the screen with **no** page
|
|
114
|
+
refresh, and the optimistic overlay does not outrank the host's declaration).
|
|
115
|
+
|
|
116
|
+
**Ports.** No change. The Python and Rust SDKs are synchronous-with-config and
|
|
117
|
+
have no mounted surfaces to notify; they expose no `convert()` and no
|
|
118
|
+
UserContext-reload API, so there is no port-side behaviour to match. The
|
|
119
|
+
decision-level contract they do share — a context carrying the new plan yields
|
|
120
|
+
decisions for that plan — is unchanged, so no parity fixture moves.
|
|
121
|
+
|
|
122
|
+
---
|
|
123
|
+
## 0.11.5
|
|
124
|
+
|
|
125
|
+
### `registerSurfaceSlot()` no longer writes to the control plane (BL-0197)
|
|
126
|
+
|
|
127
|
+
**What changed.** Outside `local_only`, `registerSurfaceSlot()` used to mirror
|
|
128
|
+
every slot to the server: `upsertSurfaceSlot()` PUT a legacy surface-slot body
|
|
129
|
+
(`{name, slug, slot_type, status, targeting_rules, content, priority,
|
|
130
|
+
metadata}`) to `/api/placements/<slot id>`, then POSTed it to `/api/placements`
|
|
131
|
+
on the 404. `/api/placements` is RevTurbine's **authored-config** CRUD; it
|
|
132
|
+
validates against `PlacementSchema`, which requires `handle` and `category`, so
|
|
133
|
+
that body could only ever be rejected. In production every non-`local_only`
|
|
134
|
+
slot registration failed with `surface_slot_create_failed:422` and the React
|
|
135
|
+
runtime turned the rejection into a `slot_error`
|
|
136
|
+
(`dogfood_event_explorer`, 2026-09-24 18:36 UTC, request
|
|
137
|
+
`030ccf51-d74b-4d55-bf4e-2a929a4ab297`).
|
|
138
|
+
|
|
139
|
+
The write was also wrong in principle. On a same-origin integration the
|
|
140
|
+
signed-in user's session cookie authenticates the request, so a body the route
|
|
141
|
+
*did* accept would have created a draft placement in the tenant's Playbook just
|
|
142
|
+
from loading a gated page. Only the 422 prevented that.
|
|
143
|
+
|
|
144
|
+
`registerSurfaceSlot()` is now **client-local**: it records the slot in the
|
|
145
|
+
in-process registry and resolves, with no network write of any kind. A write
|
|
146
|
+
happens only when you configure `endpointOverrides.surfaceSlots` (or
|
|
147
|
+
`custom_endpoints.surfaceSlots`) to point at a slot-inventory service you run —
|
|
148
|
+
the same shape `persistPlacementTypes()` already uses. There is **no fallback
|
|
149
|
+
to `/api/placements`** under any configuration.
|
|
150
|
+
|
|
151
|
+
Nothing is lost by removing it. Surface-slot discovery is **ingestion-driven**:
|
|
152
|
+
the SDK already emits `slot_evaluated` / `slot_filled` / `slot_empty` /
|
|
153
|
+
`slot_suppressed` / `slot_error` and the `placement_*` lifecycle through
|
|
154
|
+
`/api/track`, carrying `surface_slot_id`, `slot_name`, `template_ids`,
|
|
155
|
+
`surface_type`, `category`, `decision_source` and `reason_codes`, and the
|
|
156
|
+
pipeline derives discovered slots from that telemetry. `SurfaceSlotSchema` is a
|
|
157
|
+
runtime-source (DISCOVERED) schema with `first_seen` / `last_seen` — it was
|
|
158
|
+
never meant to be written by a browser.
|
|
159
|
+
|
|
160
|
+
**Landed in.** `0.11.5` (BL-0197).
|
|
161
|
+
|
|
162
|
+
**Fail-closed in.** `0.11.5` — same version. The removed path had no silent
|
|
163
|
+
mode: before this release it threw `surface_slot_create_failed:<status>` on
|
|
164
|
+
every default-transport registration, so there is no window where the old
|
|
165
|
+
behaviour was quietly tolerated. An integration that genuinely relied on the
|
|
166
|
+
write must set `endpointOverrides.surfaceSlots`; one that did not was already
|
|
167
|
+
seeing `slot_error`.
|
|
168
|
+
|
|
169
|
+
**Proving test.** `web-sdk/surface-slot-registration-no-write.test.ts` — the
|
|
170
|
+
default transport performs zero writes (fetch spy, no `/api/placements` call)
|
|
171
|
+
and resolves even when the route replays the production 404/422; the override
|
|
172
|
+
path still PUTs, and falls back to POST on the override base, never on
|
|
173
|
+
`/api/placements`. `e2e/journey.transport.spec.ts` no longer stubs
|
|
174
|
+
`/api/placements` with a 200 (the stub that hid this): it records and aborts,
|
|
175
|
+
and `assertNoPlacementWrites()` fails the test if anything calls it.
|
|
176
|
+
|
|
177
|
+
**Ports.** No change — the Python and Rust SDKs never had a slot-registration
|
|
178
|
+
write.
|
|
179
|
+
|
|
180
|
+
---
|
|
181
|
+
## 0.11.4
|
|
182
|
+
|
|
183
|
+
### The treatment-interaction wire record now carries `rule_handle` (BL-0200)
|
|
184
|
+
|
|
185
|
+
**What changed.** `flushInteractionQueue`'s projection onto
|
|
186
|
+
`POST /api/events/interactions` now includes `rule_handle`, taken from the same
|
|
187
|
+
`ruleHandle` on `RevTurbineTreatmentInteractionInput` that 0.11.3 began stamping
|
|
188
|
+
on the `placement_interaction` clickstream event.
|
|
189
|
+
|
|
190
|
+
0.11.3 deliberately kept it off this record, and #530 asserted the omission with
|
|
191
|
+
a "never reaches the treatment-interaction wire record" test. The reasoning was
|
|
192
|
+
that the clickstream carries payload fields inside a JSON column — readable the
|
|
193
|
+
moment a producer sends them — while this record is column-shaped
|
|
194
|
+
(`placement_presentations`) and a new key needed a datasource migration.
|
|
195
|
+
|
|
196
|
+
The migration has since landed (revturbine-web ledger 015 added
|
|
197
|
+
`placement_exposure_attribution.rule_handle`), and the field is now declared on
|
|
198
|
+
`TreatmentInteractionInputSchema` (`revturbine-scaffold#394`, published as
|
|
199
|
+
`@revt-eng/*` v0.1.342). Sending it lets the app record the rule an exposure was
|
|
200
|
+
**decided by**, on the base exposure row, instead of waiting for the attribution
|
|
201
|
+
worker to reconstruct one from the clickstream when a conversion lands — which
|
|
202
|
+
is what makes presentation-grain analytics cuts per rule honest rather than
|
|
203
|
+
computed over the converted subset.
|
|
204
|
+
|
|
205
|
+
**Nothing to change in your integration.** `ruleHandle` was already public and
|
|
206
|
+
already populated by every `PlacementController` path; this only widens where the
|
|
207
|
+
value is sent. Spread, not set: the key is **absent** when no decision was in
|
|
208
|
+
scope. The contract accepts an explicit `null` for "a rule was selected and none
|
|
209
|
+
matched", but the SDK cannot distinguish that from "no decision in scope" —
|
|
210
|
+
`PlacementOutput.rule_id` is simply missing in both — so it asserts neither.
|
|
211
|
+
|
|
212
|
+
**No `@public` surface moved**, hence a patch release.
|
|
213
|
+
|
|
214
|
+
## 0.11.3
|
|
215
|
+
|
|
216
|
+
### `placement_interaction` now carries `rule_handle` (BL-0182)
|
|
217
|
+
|
|
218
|
+
**What changed.** #527 (BL-0062) stamped `rule_handle` at every verdict emit
|
|
219
|
+
site by reading `decision.output.rule_id`, but `placement_interaction` is not
|
|
220
|
+
a lifecycle event and does not spread `placementLifecycleBase` — its
|
|
221
|
+
placement keys are all optional, because a bare `trackTreatmentInteraction`
|
|
222
|
+
caller may supply none of them — so the click between exposure and outcome
|
|
223
|
+
was the one step of the funnel with no rule key. That is the step
|
|
224
|
+
click-through and CTR are computed over: the funnel was sliceable by rule at
|
|
225
|
+
both ends and not in the middle.
|
|
226
|
+
|
|
227
|
+
Five emit sites now reach the same stamped payload: `trackTreatmentInteraction`
|
|
228
|
+
(the projection itself), `PlacementController.trackInteraction`
|
|
229
|
+
(dismiss / remind_me_later / cta_clicked / cta_completed — and therefore every
|
|
230
|
+
React hook and `useSurfaceSlot` / `FixedSurfaceSlot` click handler built on
|
|
231
|
+
it), `PlacementController.fireImpression`, `trackOutputInteraction`
|
|
232
|
+
(output-addressed `convert()` / `dismiss()` / `snooze()`), and the
|
|
233
|
+
server-action `trackResult` resolver.
|
|
234
|
+
|
|
235
|
+
Spread, not set: `rule_handle` is **absent** when no decision was in scope
|
|
236
|
+
(a bare `trackTreatmentInteraction` call with no placement context), and
|
|
237
|
+
`null` on the wire means "a rule was selected and none matched" — conflating
|
|
238
|
+
the two would poison the slice's coverage numbers. The unknown-output branch
|
|
239
|
+
of `trackOutputInteraction` stamps nothing on purpose: no decision from this
|
|
240
|
+
SDK produced that output, so there is no rule to name.
|
|
241
|
+
|
|
242
|
+
Companion to `revturbine-scaffold#391`, which added `rule_handle` to the
|
|
243
|
+
`placement_interaction` payload contract and `ruleHandle` to
|
|
244
|
+
`RevTurbineTreatmentInteractionInput` (published as `@revt-eng/*` v0.1.339,
|
|
245
|
+
pinned in this release).
|
|
246
|
+
|
|
247
|
+
**No wire-shape change beyond the new optional field.** `rule_handle` is
|
|
248
|
+
optional-and-nullable on the schema side (#391), which keeps every
|
|
249
|
+
already-deployed SDK out of `quarantineVerdict`. `@public` signatures are
|
|
250
|
+
unchanged (`web-sdk/generated/public-api.json` untouched) and
|
|
251
|
+
`flushInteractionQueue` still maps the treatment-interaction record field by
|
|
252
|
+
field, so `ruleHandle` never reaches a customer webhook.
|
|
253
|
+
|
|
254
|
+
**Python and Rust are unaffected.** Neither port emits events.
|
|
255
|
+
|
|
256
|
+
**Landed in** `0.11.3` (BL-0182, #530).
|
|
257
|
+
|
|
258
|
+
**Fail-closed in** not applicable — additive field, nothing old is rejected.
|
|
259
|
+
|
|
260
|
+
**Proving test:** `web-sdk/customer-side-canonical-interaction.test.ts`.
|
|
261
|
+
|
|
262
|
+
---
|
|
263
|
+
|
|
264
|
+
### A gate no longer paywalls forever when it loses the cold-start config race (BL-0179)
|
|
265
|
+
|
|
266
|
+
**What changed.** BL-0177 (below) split `config_unavailable` into a transient and
|
|
267
|
+
a terminal state for **placements** and left `checkEntitlement` on the old
|
|
268
|
+
behaviour. So the same cold-start race denied entitlements instead: while the
|
|
269
|
+
Playbook load was in flight, `checkEntitlement` — and therefore `can()`,
|
|
270
|
+
`gate()`, `useCan`, `useEntitlement` and `useGatedAction` — returned the
|
|
271
|
+
fail-closed deny with reason `config_unavailable`, which is indistinguishable
|
|
272
|
+
from a rule denial. A gate mounted before config resolution rendered its paywall
|
|
273
|
+
and never re-evaluated, hiding a feature the user had paid for.
|
|
274
|
+
|
|
275
|
+
The entitlement path now takes the same two-state split:
|
|
276
|
+
|
|
277
|
+
| state | when | behaviour |
|
|
278
|
+
|---|---|---|
|
|
279
|
+
| **transient** | a Playbook load is in flight (`getPlaybookLoadState() === 'loading'`) | `checkEntitlement` waits for it (bounded, 4s) and re-evaluates. If the load outruns that bound, `EntitlementGate` **parks** the deny instead of publishing it — `result` stays `null`, `denied` stays `false`, `isLoading` stays `true`, and **no** `gate_evaluated` is emitted — then re-checks when the load settles, at most twice per cycle. |
|
|
280
|
+
| **terminal** | no config provider is configured, or the load settled without a config | unchanged: fail-CLOSED deny, reason `config_unavailable` in Server mode / `entitlement_not_in_playbook` in local mode, with its `resolution_failure` diagnostic and exactly one `gate_evaluated` carrying the verdict (and its `rule_handle`). |
|
|
281
|
+
|
|
282
|
+
The emission rule is the one worth stating plainly: **exactly one
|
|
283
|
+
`gate_evaluated` per settled verdict, and none for the transient state** — a
|
|
284
|
+
lost race is not an evaluation and must not enter the gate funnel as a denial.
|
|
285
|
+
|
|
286
|
+
**No API, wire or reason-code change.** `@public` signatures are unchanged
|
|
287
|
+
(`web-sdk/generated/public-api.json` is untouched), both reason codes keep their
|
|
288
|
+
meanings, and `tests/reason-contract.json` is unchanged. A `limited`-but-allowed
|
|
289
|
+
result is untouched; the fail-closed contract for terminal unavailability is
|
|
290
|
+
untouched.
|
|
291
|
+
|
|
292
|
+
**Python and Rust are unaffected by design**, for the same reason as BL-0177:
|
|
293
|
+
both take the Playbook as a required constructor argument, do no I/O, and have no
|
|
294
|
+
not-yet-loaded window.
|
|
295
|
+
|
|
296
|
+
**Landed in.** `0.11.3` (BL-0179, #529).
|
|
297
|
+
|
|
298
|
+
**Fail-closed in.** Not applicable — terminal behaviour is unchanged; only the
|
|
299
|
+
transient race is now absorbed.
|
|
300
|
+
|
|
301
|
+
**Proving test.** `web-sdk/react/useCan.config-race.test.tsx` (a gate mounted
|
|
302
|
+
mid-race ends on the rule's verdict, allow and deny) and
|
|
303
|
+
`web-sdk/entitlement-config-race.test.ts` (both states, the emission rule, and
|
|
304
|
+
the bounded post-bound retry for `PlacementController` and `EntitlementGate`,
|
|
305
|
+
reached by injecting a `0` wait bound rather than with fake timers).
|
|
306
|
+
|
|
307
|
+
---
|
|
308
|
+
|
|
309
|
+
### A slot no longer paints its fallback forever when it loses the cold-start config race (BL-0177)
|
|
310
|
+
|
|
311
|
+
**What changed.** `getPlacementDecision` returns `config_unavailable` when the
|
|
312
|
+
Playbook has not arrived, uncached, specifically so a retry can succeed — but
|
|
313
|
+
nothing retried. `useSurfaceSlot` / `FixedSurfaceSlot` decide once per mount, so
|
|
314
|
+
on a cold `revturbine_server` load, whenever the first decision beat the two-hop
|
|
315
|
+
bootstrap → `/api/sdk/config` chain, the slot rendered its `fallback`
|
|
316
|
+
permanently and every integration had to hand-roll a retry.
|
|
317
|
+
|
|
318
|
+
`config_unavailable` now has two distinct states and the SDK handles each:
|
|
319
|
+
|
|
320
|
+
| state | when | behaviour |
|
|
321
|
+
|---|---|---|
|
|
322
|
+
| **transient** | a Playbook load is in flight | `getPlacementDecision` waits for it (bounded, 4s) and re-resolves. If the load outruns that bound, `PlacementController` re-decides when it settles — at most twice per load cycle. The slot stays in `isLoading` meanwhile and emits **no** `placement_resolved` / `slot_empty`: a lost race is not a resolution and must not enter the funnel as one. |
|
|
323
|
+
| **terminal** | no config provider is configured, or the load settled without a config | unchanged from before: the decision is `config_unavailable`, `visible: false`, uncached, with the resolution-failure diagnostic and the lifecycle + slot events. Bounded retries mean it settles rather than spinning. |
|
|
324
|
+
|
|
325
|
+
**No API, wire or reason-code change.** `config_unavailable` is still the reason
|
|
326
|
+
code, the decision shape is untouched, and `tests/reason-contract.json` is
|
|
327
|
+
unchanged. Customer code that already hand-rolls a retry keeps working — it is
|
|
328
|
+
now redundant, not wrong.
|
|
329
|
+
|
|
330
|
+
**Python and Rust are unaffected by design.** Both ports take the Playbook as a
|
|
331
|
+
required constructor argument and raise on a missing or malformed one
|
|
332
|
+
(`server-python/src/revturbine/sdk.py` `__init__`, `server-rust/src/sdk.rs`
|
|
333
|
+
`new`), perform no I/O, and never emit `config_unavailable` — there is no
|
|
334
|
+
not-yet-loaded window to retry. This is a browser/hosted-mode state only.
|
|
335
|
+
|
|
336
|
+
**Landed in.** `0.11.3` (BL-0177, #528).
|
|
337
|
+
|
|
338
|
+
**Fail-closed in.** Not applicable — the terminal behaviour is unchanged; only
|
|
339
|
+
the transient race is now absorbed.
|
|
340
|
+
|
|
341
|
+
**Proving test.** `web-sdk/placements/FixedSurfaceSlot.config-race.test.tsx`
|
|
342
|
+
(both states), plus `e2e/journey.transport.spec.ts`, whose two hand-rolled retry
|
|
343
|
+
loops were deleted — a retry loop reappearing there is the regression signal.
|
|
344
|
+
|
|
345
|
+
---
|
|
346
|
+
|
|
347
|
+
## 0.11.1
|
|
348
|
+
|
|
349
|
+
### The generated schema *types* gained their `Playbook*` names (BL-0165)
|
|
350
|
+
|
|
351
|
+
**What changed.** `0.11.0` renamed every option, keyword and method that carries a
|
|
352
|
+
Playbook. It did not rename the **generated type identifiers**, which come from
|
|
353
|
+
scaffold and are vendored here — so a `0.11.0` integration still had to import
|
|
354
|
+
`ExportedConfigSegmentsItem` to name a segment. Those types now have `Playbook*`
|
|
355
|
+
spellings too.
|
|
356
|
+
|
|
357
|
+
| deprecated | canonical |
|
|
358
|
+
|---|---|
|
|
359
|
+
| `ExportedConfigSegmentsItem` | `PlaybookSegmentsItem` |
|
|
360
|
+
| `ExportedConfigSegmentsItemPredicatesItem` | `PlaybookSegmentsItemPredicatesItem` |
|
|
361
|
+
| `ExportedConfigPlacementItem` | `PlaybookPlacementItem` |
|
|
362
|
+
| `ExportedConfigUiPathActionType` | `PlaybookUiPathActionType` |
|
|
363
|
+
| `ExportedConfig` | `Playbook` (already shipped in `0.11.0`) |
|
|
364
|
+
|
|
365
|
+
**This renames nothing on the wire.** The names are generated from exported keys
|
|
366
|
+
of scaffold's Zod barrel: both spellings resolve to the **same schema object**, so
|
|
367
|
+
no payload field, default or validation rule differs between them, and the OpenAPI
|
|
368
|
+
component names are unchanged (they come from `.meta({ id })` on the underlying
|
|
369
|
+
schemas, not from the key). A Playbook written by an older SDK parses byte-for-byte
|
|
370
|
+
identically. Scaffold's payload contract — `bundle_schema_version` /
|
|
371
|
+
`bundle_min_readable_schema_version` — is untouched, so there is no minimum-reader
|
|
372
|
+
floor to raise.
|
|
373
|
+
|
|
374
|
+
**The aliases are duplicate definitions, not type aliases.** Scaffold's generators
|
|
375
|
+
emit one full TS type, Pydantic model and serde struct per barrel key rather than an
|
|
376
|
+
alias, so in Python (`revturbine.types`) and Rust (`revturbine::types`) the old
|
|
377
|
+
names remain as their own complete models/structs, generated from the same schema.
|
|
378
|
+
No hand-written shim exists in either port, and none is wanted: it would collide
|
|
379
|
+
with the generated definition. In TypeScript the `core` barrel re-exports the old
|
|
380
|
+
pair with `@deprecated` TSDoc.
|
|
381
|
+
|
|
382
|
+
Requires `@revt-eng/schema` / `@revt-eng/core` ≥ 0.1.335, which added the
|
|
383
|
+
`Playbook*` keys upstream (scaffold #387).
|
|
384
|
+
|
|
385
|
+
Ruling: Kent, 2026-09-23 (BL-0156 — `ExportedConfig` naming deprecated, Playbook
|
|
386
|
+
canonical).
|
|
387
|
+
|
|
388
|
+
**Landed in** `0.11.1` (additive — every old name still resolves).
|
|
389
|
+
**Fail-closed in** `0.12.0`, when BL-0167 removes the `ExportedConfig*` keys from
|
|
390
|
+
scaffold's barrel and the generated definitions disappear from the vendored types
|
|
391
|
+
along with them. That is the same window as the `0.11.0` option/method aliases, so
|
|
392
|
+
an integration has one removal to absorb, not two.
|
|
393
|
+
|
|
394
|
+
**Proving test:** scaffold's `src/core/zod/playbook-generated-names.test.ts` (each
|
|
395
|
+
`Playbook*` key is the same object as its `RevTurbineConfig*` and `ExportedConfig*`
|
|
396
|
+
counterparts; a legacy `export-config.json` payload parses to an equal result
|
|
397
|
+
through both spellings of the config schema), plus this repo's
|
|
398
|
+
`scripts/check-vendored-types.mjs` (the vendored py/rs copies agree with the pin).
|
|
399
|
+
|
|
400
|
+
**Public-API snapshot: unchanged.** `pnpm check:public-api` still reports the same
|
|
401
|
+
**62** `@public` exports and `web-sdk/generated/public-api.json` is byte-identical,
|
|
402
|
+
because the `core` barrel's type re-exports carry no `@public` tag. Recorded here
|
|
403
|
+
rather than left implicit: a rename touching the SDK's type vocabulary that moves
|
|
404
|
+
*no* snapshot entry is worth stating, so a reader does not go looking for the diff.
|
|
405
|
+
No existing export's signature changed and none was removed.
|
|
406
|
+
|
|
407
|
+
## 0.11.0
|
|
408
|
+
|
|
409
|
+
### `Playbook` is the canonical name for every option that carries one (BL-0156)
|
|
410
|
+
|
|
411
|
+
**What changed.** `ExportedConfig` is dead vocabulary. Plan 118 renamed the
|
|
412
|
+
domain object to **Playbook** and plan 104 renamed the schema type to
|
|
413
|
+
`RevTurbineConfig`, but the *option*, *keyword* and *method* names that carry a
|
|
414
|
+
Playbook around the SDK still spelled it `exportedConfig` / `exported_config`.
|
|
415
|
+
Every one of them now has a `Playbook`-named counterpart; every old name stays
|
|
416
|
+
as a deprecated alias that still works.
|
|
417
|
+
|
|
418
|
+
TypeScript:
|
|
419
|
+
|
|
420
|
+
| deprecated | canonical |
|
|
421
|
+
|---|---|
|
|
422
|
+
| `localRuntime.exportedConfig` | `localRuntime.playbook` (already shipped; see below) |
|
|
423
|
+
| `localRuntime.resolvers.resolveExportedConfig` | `localRuntime.resolvers.resolvePlaybook` |
|
|
424
|
+
| `configProvider.getExportedConfig()` | `configProvider.getPlaybook()` |
|
|
425
|
+
| `sdk.getExportedConfig()` | `sdk.getPlaybook()` |
|
|
426
|
+
| `getPolicy().exportedConfigVersion` | `getPolicy().playbookVersion` |
|
|
427
|
+
| `BrowserRuntimeOptions.exportedConfig` | `BrowserRuntimeOptions.playbook` |
|
|
428
|
+
| `LocalEvaluationServerOptions.exportedConfig` | `LocalEvaluationServerOptions.playbook` |
|
|
429
|
+
| `ExportedConfigProvider` (type) | `RevTurbineConfigProvider`, also exported as `PlaybookProvider` |
|
|
430
|
+
| `ExportedConfig` (type) | `Playbook` |
|
|
431
|
+
|
|
432
|
+
Python: `RevTurbineCustomerSdk(exported_config=…)`, `LocalRuntime(exported_config=…)`,
|
|
433
|
+
`LocalRuntime.get_exported_config()`, `create_static_placement_resolver(exported_config=…)`,
|
|
434
|
+
`derive_local_entitlement_from_configured_rules(exported_config=…)`,
|
|
435
|
+
`configured_plan_name_from_exported_config`, `parse_exported_config_or_throw` and
|
|
436
|
+
`static.ExportedConfig` all gained `playbook`-spelled counterparts.
|
|
437
|
+
|
|
438
|
+
Rust: `configured_plan_name_from_exported_config` is now
|
|
439
|
+
`configured_plan_name_from_playbook`, with the old name kept as the crate's
|
|
440
|
+
first `#[deprecated]` item. Rust has no keyword arguments, so nothing else in
|
|
441
|
+
that port was a caller-visible name.
|
|
442
|
+
|
|
443
|
+
`RevTurbineConfigProvider`'s two accessors are both OPTIONAL on the interface so
|
|
444
|
+
that an existing `implements RevTurbineConfigProvider` clause keeps compiling;
|
|
445
|
+
the `configProvider` option intersects it with a union that still requires
|
|
446
|
+
exactly one of them, so a provider implementing neither is a compile error where
|
|
447
|
+
it is passed.
|
|
448
|
+
|
|
449
|
+
Reading a deprecated name logs **one** development-build warning per runtime
|
|
450
|
+
(one `DeprecationWarning` per process on Python) naming the canonical
|
|
451
|
+
replacement — one flag for every alias, so an integration still spelling several
|
|
452
|
+
of them gets a single line, not a wall. Production builds are silent. Requires
|
|
453
|
+
`@revt-eng/core` ≥ 0.1.331, which made the same rename upstream (scaffold #380).
|
|
454
|
+
|
|
455
|
+
Ruling: Kent, 2026-09-23 (BL-0156).
|
|
456
|
+
|
|
457
|
+
**Landed in** `0.11.0`. **Fail-closed in** `0.12.0` for the `ExportedConfig`
|
|
458
|
+
aliases — until then every old name works and warns once.
|
|
459
|
+
|
|
460
|
+
**Proving test:** `web-sdk/playbook-option.test.ts` (canonical never warns, the
|
|
461
|
+
alias still resolves, one warning across three read sites, both spellings reach
|
|
462
|
+
the same Playbook at every public read site),
|
|
463
|
+
`server-python/tests/test_playbook_option.py` (the same contract plus
|
|
464
|
+
`get_exported_config()`), and `server-rust/src/user_context.rs`
|
|
465
|
+
`playbook_rename_tests` (the alias returns exactly what the canonical returns).
|
|
466
|
+
|
|
467
|
+
### `localRuntime.playbook` is the canonical local-mode config key
|
|
468
|
+
|
|
469
|
+
**What changed.** The record this file owed and never carried: `localRuntime.playbook`
|
|
470
|
+
shipped earlier as the canonical spelling of `localRuntime.exportedConfig`, with
|
|
471
|
+
`resolveLocalPlaybook()` as the single resolver deciding precedence, but no
|
|
472
|
+
changelog entry was ever written for it — so an integration could not date the
|
|
473
|
+
change. It is recorded here rather than back-dated, because the version it
|
|
474
|
+
landed in is not the version this entry appears in and pretending otherwise is
|
|
475
|
+
exactly the confusion the "Landed in" / "Fail-closed in" split exists to
|
|
476
|
+
prevent. As of `0.11.0` the alias also warns once (it did not before).
|
|
477
|
+
|
|
478
|
+
**Landed in** a release before `0.11.0` (additive; the alias never stopped
|
|
479
|
+
working). **Fail-closed in** `0.12.0`, with the rest of the `ExportedConfig`
|
|
480
|
+
aliases.
|
|
481
|
+
|
|
482
|
+
**Proving test:** `web-sdk/playbook-option.test.ts` — `resolveLocalPlaybook`
|
|
483
|
+
accepts either key, prefers `playbook`, and warns only for the alias.
|
|
484
|
+
|
|
485
|
+
### `publicKey` is the only browser credential; `ingestPublicKey` is gone (BL-0113)
|
|
486
|
+
|
|
487
|
+
**What changed.** `0.10.0` made `publicKey` the browser credential and kept
|
|
488
|
+
`apiKey` and `ingestPublicKey` working as aliases "for one minor". This is that
|
|
489
|
+
minor:
|
|
490
|
+
|
|
491
|
+
- **`ingestPublicKey` is removed from `RevTurbineInitOptions`.** Passing it is
|
|
492
|
+
now a **type error**, including when the options are built in a variable
|
|
493
|
+
(`ExactInitOptions` rejects it there too).
|
|
494
|
+
- **`apiKey` is no longer read as a browser credential.** It still exists, and
|
|
495
|
+
still means exactly one thing: the secret **server key**, for
|
|
496
|
+
`@revturbine/sdk/server` and for the headless SDK on a backend. A browser init
|
|
497
|
+
that supplies only `apiKey` now resolves **no** browser credential and fails
|
|
498
|
+
at init, instead of silently sending a server key as the bearer.
|
|
499
|
+
|
|
500
|
+
`resolveBrowserPublicKey()` reads `publicKey` and nothing else, and
|
|
501
|
+
`resetBrowserKeyAliasWarning()` is gone with the warning it reset. Keyless
|
|
502
|
+
local-only init is unchanged, and the plan 95 anonymous `sdk_init` beacon now
|
|
503
|
+
keys off `publicKey` alone.
|
|
504
|
+
|
|
505
|
+
Ruling: Kent, 2026-09-23 (BL-0113), closing the window plan 257 opened.
|
|
506
|
+
|
|
507
|
+
**Landed in** `0.11.0`. **Fail-closed in** `0.11.0` — the same release. This IS
|
|
508
|
+
the fail-closed half of the `0.10.0` entry below; there is no further tolerance
|
|
509
|
+
window.
|
|
510
|
+
|
|
511
|
+
**Proving test:** `web-sdk/public-key-option.test.ts` (`publicKey` is the bearer
|
|
512
|
+
on ingest and every control-plane fetch; `apiKey` resolves nothing; nothing
|
|
513
|
+
warns), `web-sdk/customer-side-ingest.test.ts` (the bearer no longer carries the
|
|
514
|
+
server key), and `web-sdk/init-options-exactness.test-d.ts` (`ingestPublicKey`
|
|
515
|
+
is a type error inline AND in a variable — if the key is ever re-added, `tsc`
|
|
516
|
+
fails on the unused `@ts-expect-error`).
|
|
517
|
+
|
|
518
|
+
---
|
|
519
|
+
|
|
520
|
+
## 0.10.10
|
|
521
|
+
|
|
522
|
+
### Trial tokens now reach placement content, not only `getPersonalizationTokens()` (BL-0169)
|
|
523
|
+
|
|
524
|
+
**What changed.** A placement whose copy authors `{{trial_days_remaining}}` or
|
|
525
|
+
`{{trial_days_total}}` now renders the live values. Content interpolation sources
|
|
526
|
+
its token map from the **output content itself**, so a provider-derived token
|
|
527
|
+
only reaches the copy if the resolver writes it onto that content first. The
|
|
528
|
+
usage lane always did (`usage_current`, `usage_limit`, `usage_percent`,
|
|
529
|
+
`usage_remaining`, `reset_date`); the trial lane never did, so a `trial_ending`
|
|
530
|
+
body reading `"{{trial_days_remaining}} days left"` shipped the literal
|
|
531
|
+
`{{trial_days_remaining}}` to the end user — on every port. The two names were
|
|
532
|
+
derived for `getPersonalizationTokens()` all along, which is why the gap survived:
|
|
533
|
+
a host that read the token map saw the right numbers and a host that authored the
|
|
534
|
+
token into copy did not.
|
|
535
|
+
|
|
536
|
+
| | before | now |
|
|
537
|
+
|---|---|---|
|
|
538
|
+
| `getPersonalizationTokens().trial_days_remaining` | live value | live value (unchanged) |
|
|
539
|
+
| content `{{trial_days_remaining}}` / `{{trial_days_total}}` | literal `{{…}}` | live value |
|
|
540
|
+
| `outputContent.trial_days_remaining` / `_total` | absent | written when the plan provider carries a finite number |
|
|
541
|
+
|
|
542
|
+
Identical on all three ports (`ts:local-resolver.ts`,
|
|
543
|
+
`py:core/placements/local_resolver.py`, `rs:placements/static_resolver.rs`): the
|
|
544
|
+
same two token names, a `Number.isFinite` guard, provider state winning over an
|
|
545
|
+
authored content value of the same key, and the value passed through **without
|
|
546
|
+
widening** — `day_number` 7 + `days_remaining` 3 is `trial_days_total` `10`, never
|
|
547
|
+
`10.0` (BL-0155).
|
|
548
|
+
|
|
549
|
+
**Who this reaches.** Any host whose placement copy authors either token. Copy
|
|
550
|
+
that hard-coded one of those keys as a decorative literal now has it overwritten
|
|
551
|
+
by live state — the same precedence the usage lane and
|
|
552
|
+
`derivePlacementPersonalizationTokens` have always had. `{{trial_plan_name}}` and
|
|
553
|
+
`{{trial_features_used}}` remain **not yet wired** by the placement-studio-ui
|
|
554
|
+
spec's own note and are untouched.
|
|
555
|
+
|
|
556
|
+
**Landed in.** `0.10.10`.
|
|
557
|
+
|
|
558
|
+
**Fail-closed in.** `0.10.10` — absence still leaves the raw token standing
|
|
559
|
+
rather than rendering `0`, so a host with no trial provider state sees exactly
|
|
560
|
+
what it saw before.
|
|
561
|
+
|
|
562
|
+
**Proving test.** Parity scenario `trial_tokens_in_content` under
|
|
563
|
+
`"normalize": "preserve-representation"` (byte-locked ts ≡ py ≡ rs), plus
|
|
564
|
+
`py:tests/placements/test_local_resolver.py` and `rs:tests/static_resolver.rs`
|
|
565
|
+
`trial_tokens_are_injected_from_plan_provider_state` and its two neighbours,
|
|
566
|
+
mirroring the scaffold-side `local-resolver.test.ts` cases. Needs
|
|
567
|
+
`@revt-eng/core` 0.1.331 or later (scaffold #382) for the TS half; this release
|
|
568
|
+
pins 0.1.332.
|
|
569
|
+
|
|
570
|
+
---
|
|
571
|
+
|
|
572
|
+
## 0.10.9
|
|
573
|
+
|
|
574
|
+
### The trial-status overlay no longer changes a number's type (BL-0155)
|
|
575
|
+
|
|
576
|
+
**What changed.** The `UserTrialStatus` → `trial_*` PlanProviderState overlay now
|
|
577
|
+
passes each numeric field through on every port, instead of two ports widening
|
|
578
|
+
it to a float. The canonical behaviour is the TS SDK's
|
|
579
|
+
`synthesizeProviderContext` (`web-sdk/customer-side.ts`, the `planTrialFields`
|
|
580
|
+
block), which spreads every `UserTrialStatus` member verbatim: an integer
|
|
581
|
+
`progress_percent` stays an integer on the provider state, and
|
|
582
|
+
`trial_days_total` is `day_number + days_remaining` in the inputs' own kind, so
|
|
583
|
+
`7 + 3` is `14`. The two server ports diverged from it:
|
|
584
|
+
|
|
585
|
+
| field | Python before | Rust before | all three now |
|
|
586
|
+
|---|---|---|---|
|
|
587
|
+
| `progress_percent` → `trial_progress_percent` | `float(v)` | verbatim | verbatim |
|
|
588
|
+
| `days_remaining` → `trial_days_remaining` | `float(v)` | verbatim | verbatim |
|
|
589
|
+
| `usage_consumed` → `trial_usage_consumed` | `float(v)` | verbatim | verbatim |
|
|
590
|
+
| `usage_limit` → `trial_usage_limit` | `float(v)` | verbatim | verbatim |
|
|
591
|
+
| `in_trial` → `trial_active` | `bool(v)` | verbatim | verbatim |
|
|
592
|
+
| derived `trial_days_total` | `float(a) + float(b)` | `f64` sum | `int + int` → `int`; any fractional half → float |
|
|
593
|
+
|
|
594
|
+
**Who this reaches.** Only a host that reads the provider state it built —
|
|
595
|
+
`revturbine::overlay_trial_status_on_plan_provider` on Rust, or the merged
|
|
596
|
+
`PlanProviderState` on Python — and cares whether it holds `70` or `70.0`
|
|
597
|
+
(serializing it, comparing it to an integer, or logging it). No decision
|
|
598
|
+
changes: every reader in the trial-gating layer widens on read, which is exactly
|
|
599
|
+
why the divergence survived. A non-numeric `day_number`/`days_remaining` half
|
|
600
|
+
now derives no total on Python instead of raising, matching Rust's existing
|
|
601
|
+
numeric guard.
|
|
602
|
+
|
|
603
|
+
**Landed in.** `0.10.9`.
|
|
604
|
+
|
|
605
|
+
**Fail-closed in.** `0.10.9` — there was no tolerated-old-shape window; the
|
|
606
|
+
representation simply changes.
|
|
607
|
+
|
|
608
|
+
**Proving test.**
|
|
609
|
+
`server-python/tests/test_trial_overlay_upsert.py::test_overlay_preserves_integer_numeric_representation`
|
|
610
|
+
and `server-rust/src/sdk.rs::overlay_preserves_integer_numeric_representation`
|
|
611
|
+
— both assert the *type*, not just the value, because `100 == 100.0` in Python.
|
|
612
|
+
Scenario `trial_overlay_integer_fields` locks that the three ports still decide
|
|
613
|
+
identically from an all-integer trial status.
|
|
614
|
+
|
|
615
|
+
> **Correction, 2026-09-23 (BL-0158, harness-only — no release).** This entry
|
|
616
|
+
> originally said the parity corpus *cannot* see this class of divergence,
|
|
617
|
+
> because `tests/parity/normalize.*` rule 4 collapses integral floats by design
|
|
618
|
+
> and nothing in the corpus returned the overlaid provider state. Both halves
|
|
619
|
+
> are now closed and the sentence no longer holds:
|
|
620
|
+
> `trial_overlay_integer_fields` drives `resolveProviders` (already shipped on
|
|
621
|
+
> all three runtimes) under a new per-scenario
|
|
622
|
+
> `"normalize": "preserve-representation"` flag that turns the collapse off, so
|
|
623
|
+
> `14` and `14.0` are different bytes and the cross-language byte-diff fails on
|
|
624
|
+
> a widened field. No public API changed; the harness gained the ability to see
|
|
625
|
+
> provider state.
|
|
626
|
+
|
|
627
|
+
---
|
|
628
|
+
|
|
629
|
+
## 0.10.8
|
|
630
|
+
|
|
631
|
+
### `{{recommended_plan_name}}` and `{{plan_name}}` no longer reach end users as raw tokens (BL-0121)
|
|
632
|
+
|
|
633
|
+
**What changed.** The placement-decision lane now substitutes the five tokens
|
|
634
|
+
the SDK resolves from the Playbook — `plan_name`, `plan_price`,
|
|
635
|
+
`upgrade_plan_price`, `recommended_plan_handle`, `recommended_plan_name` —
|
|
636
|
+
into `RevTurbinePlacementDecision.content` (and the mirrored
|
|
637
|
+
`decision.output.content`). It previously substituted `plan_price` and
|
|
638
|
+
`upgrade_plan_price` only.
|
|
639
|
+
|
|
640
|
+
`recommended_plan_handle` / `recommended_plan_name` were already derived, by
|
|
641
|
+
the parity-locked `resolveRecommendedPlanTokens` dispatch, and already readable
|
|
642
|
+
via `getPersonalizationTokens()`. Nothing wrote them into rendered content, so
|
|
643
|
+
a placement authored with `"Upgrade to {{recommended_plan_name}}"` shipped the
|
|
644
|
+
literal braces to the end user. Observed on the CybeDefend demo, 2026-09-22.
|
|
645
|
+
|
|
646
|
+
Two properties of the substitution are deliberate:
|
|
647
|
+
|
|
648
|
+
| input | result |
|
|
649
|
+
|---|---|
|
|
650
|
+
| 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 |
|
|
651
|
+
| a token the SDK does not own (`{{usage_percent}}`, an app-defined token) | left verbatim, so the React render lane still resolves it |
|
|
652
|
+
|
|
653
|
+
`plan_name` resolves through the plan's `unique_handle`, the matching identity
|
|
654
|
+
— the lookup used to be handed the plan *object*, which by design resolves
|
|
655
|
+
nothing, so `{{plan_name}}` rendered raw for every user identified by
|
|
656
|
+
`plan_handle`.
|
|
657
|
+
|
|
658
|
+
**Landed in.** `0.10.8`.
|
|
659
|
+
|
|
660
|
+
**Not fixed here.** `{{upgrade_plan_price}}` still renders the ANNUAL amount
|
|
661
|
+
for a plan that has both an annual and a monthly variation. Variation
|
|
662
|
+
preference keys off the user's billing cadence, but no supported input carries
|
|
663
|
+
it (`billing_period` is absent from `UserContextSchema` and from the recognized
|
|
664
|
+
`identify()` / `update()` keys), so selection falls through to the
|
|
665
|
+
alphabetically-first variation and `<plan>_annual` always wins. Closing it
|
|
666
|
+
needs a schema field for the cadence and a contract for which period the token
|
|
667
|
+
reflects when the cadence is unknown; both are open on BL-0121.
|
|
668
|
+
|
|
669
|
+
**Proving test.** `web-sdk/customer-side-upgrade-tokens.test.ts`.
|
|
670
|
+
|
|
671
|
+
---
|
|
672
|
+
|
|
673
|
+
## 0.10.7
|
|
674
|
+
|
|
675
|
+
### The Python and Rust ports stop filtering payloads on `status` (BL-0151)
|
|
676
|
+
|
|
677
|
+
**What changed.** Payload `status` is no longer a runtime predicate in any port.
|
|
678
|
+
Python and Rust required `status == "active"` on a placement payload before it
|
|
679
|
+
could become a candidate, and read a `status` off each content-linked studio
|
|
680
|
+
payload before overlaying its copy. Both now ignore it, matching the TypeScript
|
|
681
|
+
port, which never gated on it.
|
|
682
|
+
|
|
683
|
+
| | Before | After |
|
|
684
|
+
|---|---|---|
|
|
685
|
+
| Placement payload candidacy (Python, Rust) | required `status == "active"` | every payload, status ignored |
|
|
686
|
+
| Content-linked copy overlay (Python, Rust) | required the studio payload's `status` to read `active` | always overlaid |
|
|
687
|
+
| TypeScript | already ignored `status` | unchanged |
|
|
688
|
+
|
|
689
|
+
**Why it mattered.** `status` is not a field of the exported Playbook.
|
|
690
|
+
`RevTurbineConfigStudioPayload` and `RevTurbineConfigPlacementPayloadItem` carry
|
|
691
|
+
no `status` key, unknown keys are stripped on parse, and the published JSON
|
|
692
|
+
Schema is `additionalProperties: false` — because runtime status is *derived*
|
|
693
|
+
control-plane side and the stored column was dropped. So `status == "active"` was
|
|
694
|
+
false for **every** payload of a real Playbook: the Python and Rust resolvers
|
|
695
|
+
indexed no candidates at all and returned `placement_not_found`, and their
|
|
696
|
+
content-lookup adapters dropped every content-linked payload, keeping the inline
|
|
697
|
+
copy where TypeScript overlaid the linked block. A config whose first payload
|
|
698
|
+
carried a stray authored `status` also decided differently across ports.
|
|
699
|
+
|
|
700
|
+
The cross-language parity gate could not see any of this: the only fixtures
|
|
701
|
+
reaching these paths hand-write `status` on each payload — a key the schema would
|
|
702
|
+
strip — so the ports were compared on a shape no Playbook has.
|
|
703
|
+
|
|
704
|
+
**Landed in.** 0.10.7 (`server-python`, `server-rust`). No `@revt-eng/core`
|
|
705
|
+
change rides with it: TypeScript was already correct, so there is no scaffold
|
|
706
|
+
release and no pin bump.
|
|
707
|
+
|
|
708
|
+
**Fail-closed in.** n/a — this release removes a fail-*closed* filter. Python and
|
|
709
|
+
Rust consumers on a real Playbook go from no decision to the decision
|
|
710
|
+
TypeScript has always returned.
|
|
711
|
+
|
|
712
|
+
**Proving test.** `tests/parity/fixtures/payload_status_filter.json` — two lanes:
|
|
713
|
+
a stray authored `status` that disagrees with drag order (payload 1 `draft`,
|
|
714
|
+
payload 2 `active`; payload 1 must still win), and the real wire shape with no
|
|
715
|
+
`status` anywhere plus a content-linked overlay. Mutation-checked by reinstating
|
|
716
|
+
the Python filter, which diverges on the first call. Plus
|
|
717
|
+
`server-python/tests/placements/test_local_resolver.py`,
|
|
718
|
+
`test_json_content_provider.py` and `server-rust/tests/static_resolver.rs`, each
|
|
719
|
+
covering both the stray-value and the no-key case.
|
|
720
|
+
|
|
721
|
+
**Who is affected.** Every `revturbine` (PyPI) and `revturbine` (crates.io)
|
|
722
|
+
consumer resolving placements from a schema-valid exported Playbook — that is,
|
|
723
|
+
one produced by the control plane rather than hand-written with a `status` field.
|
|
724
|
+
Those integrations were receiving no placement decisions at all.
|
|
725
|
+
|
|
726
|
+
---
|
|
727
|
+
|
|
728
|
+
## 0.10.6
|
|
729
|
+
|
|
730
|
+
### The trial-status PlanProvider overlay ships in the Rust crate (BL-0153)
|
|
731
|
+
|
|
732
|
+
**What changed.** `revturbine::overlay_trial_status_on_plan_provider` is a new
|
|
733
|
+
public function on the Rust crate: it maps a runtime `UserTrialStatus` onto the
|
|
734
|
+
`trial_*` fields of a resolved PlanProviderState, the fields the placement
|
|
735
|
+
resolver's `trial_progress` / `trial_ending` / `trial_ended` /
|
|
736
|
+
`trial_converted` gates and milestone supersession read. Python has always done
|
|
737
|
+
this inside its shipped package
|
|
738
|
+
(`revturbine.sdk._overlay_trial_status_on_plan_provider`); on Rust the mapping
|
|
739
|
+
existed only as a private helper the facade used, so a host that assembles its
|
|
740
|
+
own provider context — including this repo's own parity runner — had to
|
|
741
|
+
re-implement it. Additive: no existing signature changed.
|
|
742
|
+
|
|
743
|
+
Porting Python's mapping faithfully corrected two Rust-only divergences in the
|
|
744
|
+
overlay the facade applies:
|
|
745
|
+
|
|
746
|
+
| `UserTrialStatus` field | Rust before | Rust now (= TS/Python) |
|
|
747
|
+
|---|---|---|
|
|
748
|
+
| `day_number` + `days_remaining` | not derived | `trial_days_total = day_number + days_remaining`, only when both are present |
|
|
749
|
+
| `day_number` | written to `trial_day_number` | not written — no provider-state field reads it |
|
|
750
|
+
| `usage_entitlement_handle` | dropped | `trial_usage_entitlement_handle` |
|
|
751
|
+
|
|
752
|
+
`trial_days_total` is the input to the time-mode progress fallback in
|
|
753
|
+
`placements::trial_gating`, so before this the fallback could never fire on the
|
|
754
|
+
Rust port.
|
|
755
|
+
|
|
756
|
+
**Landed in.** `0.10.6`.
|
|
757
|
+
|
|
758
|
+
**Fail-closed in.** `0.10.6` — the parity facade-presence gate
|
|
759
|
+
(`tests/parity/facade-surface.json`) no longer allow-lists a runner-local
|
|
760
|
+
overlay, so a future re-implementation harness-side fails the gate.
|
|
761
|
+
|
|
762
|
+
**Proving test.** `server-rust/src/sdk.rs` `overlay_maps_every_canonical_trial_field`,
|
|
763
|
+
`trial_days_total_requires_both_halves`, `overlay_does_not_clobber_base_state_with_null`
|
|
764
|
+
(mirroring `server-python/tests/test_trial_overlay_upsert.py`), plus the
|
|
765
|
+
`trial_ending_days_before_end`, `trial_ended_post_expiry` and
|
|
766
|
+
`trial_progress_milestone_supersession` parity fixtures.
|
|
767
|
+
|
|
768
|
+
---
|
|
769
|
+
|
|
770
|
+
## 0.10.5
|
|
771
|
+
|
|
772
|
+
### Trial-only local integrations get provider context (BL-0120)
|
|
773
|
+
|
|
774
|
+
**What changed.** `synthesizeProviderContext()` — the fallback that builds
|
|
775
|
+
`DomainProvider` state from `userContext` when no explicit domain provider is
|
|
776
|
+
registered — dropped an integration's trial data whenever no commercial
|
|
777
|
+
`plan` / `plan_handle` was also present. An app supplying **only**
|
|
778
|
+
`initialData.trialStatus` (or `setTrialInstances()` / `setTrialStatus()` with
|
|
779
|
+
no plan) got no provider context at all: `providers.plan` came back
|
|
780
|
+
`undefined`, so `{{trial_days_remaining}}` (and every other `trial*` token
|
|
781
|
+
`derivePlacementPersonalizationTokens` derives from `providers.plan`) stayed
|
|
782
|
+
raw, unresolved, in placement copy. Reported against the CybeDefend demo
|
|
783
|
+
(SDK 0.7.13): a `trial_ending` placement with `in_trial: true,
|
|
784
|
+
days_remaining: 3` left `{{trial_days_remaining}}` literal in the rendered
|
|
785
|
+
body.
|
|
786
|
+
|
|
787
|
+
Two omissions, same root cause — trial was never checked alongside
|
|
788
|
+
`plan` / `plan_handle` / `usage` / `tiers` / `experiments`:
|
|
789
|
+
|
|
790
|
+
- the early-return guard (`if (!plan && !planHandle && !usage && !hasTiers
|
|
791
|
+
&& !hasExperiments) return undefined;`) treated a trial-only integration as
|
|
792
|
+
having no signal whatsoever;
|
|
793
|
+
- even past that guard, the returned `plan` key itself was gated on
|
|
794
|
+
`plan || planHandle` — so the trial fields (`trialActive`,
|
|
795
|
+
`trialDaysRemaining`, etc.) it computed were still discarded for a
|
|
796
|
+
plan-less trial.
|
|
797
|
+
|
|
798
|
+
Both gates now also check `hasTrial` (an explicit trial signal — the default
|
|
799
|
+
untouched `{ in_trial: false }` sentinel does not count, so the omitted-signal
|
|
800
|
+
short-circuit is otherwise unchanged for integrations that supply nothing at
|
|
801
|
+
all). `usage`-only, `tiers`-only, and `experiments`-only integrations already
|
|
802
|
+
threaded through correctly before this change; trial was the only signal
|
|
803
|
+
missing from both checks.
|
|
804
|
+
|
|
805
|
+
No public API change — `synthesizeProviderContext` is private, and the fix is
|
|
806
|
+
additive: a plan-less trial integration now gets a `providers.plan` it never
|
|
807
|
+
got before, with an empty `currentPlanHandle: ''` (no plan name/handle was
|
|
808
|
+
ever supplied to derive one from).
|
|
809
|
+
|
|
810
|
+
**Landed in** `0.10.5`. **Fail-closed in** n/a — a previously-dropped signal
|
|
811
|
+
is now included; nothing that resolved before stops resolving.
|
|
812
|
+
|
|
813
|
+
**Proving test:** `web-sdk/customer-side-trial-token-signal.test.ts` — a
|
|
814
|
+
trial-only local integration now gets a non-empty provider context with
|
|
815
|
+
`trialActive`/`trialDaysRemaining` set, and feeding those through
|
|
816
|
+
`derivePlacementPersonalizationTokens` + `resolveContent` renders
|
|
817
|
+
`{{trial_days_remaining}}` as `"3"` instead of leaving it literal. Includes
|
|
818
|
+
the positive control (trial + plan together — unchanged), the negative
|
|
819
|
+
control (no trial data at all — the token stays absent, never coerced to
|
|
820
|
+
`"0"`), and a sibling check confirming usage-only integrations were already
|
|
821
|
+
unaffected by the guard.
|
|
822
|
+
|
|
823
|
+
---
|
|
824
|
+
|
|
825
|
+
## 0.10.4
|
|
826
|
+
|
|
827
|
+
### Every payload is a selection candidate, not just the first (BL-0122)
|
|
828
|
+
|
|
829
|
+
**What changed.** A placement's payloads are now *all* candidates on both
|
|
830
|
+
selection paths, in all three ports. Previously each port considered exactly one
|
|
831
|
+
payload per placement — TypeScript took `payloads[0]`, Python and Rust the first
|
|
832
|
+
payload with `status == "active"` — so payloads 2+ were never candidates and
|
|
833
|
+
their `target.segment_chips` could not be evaluated at all.
|
|
834
|
+
|
|
835
|
+
| | Before | After |
|
|
836
|
+
|---|---|---|
|
|
837
|
+
| Candidacy | one payload per placement | every payload (Python/Rust: every `active` payload) |
|
|
838
|
+
| `target.segment_chips` on payloads 2+ | never evaluated | evaluated per payload |
|
|
839
|
+
| Drag precedence | a pre-filter — it chose the only candidate | a tiebreaker among the payloads the user **matches** |
|
|
840
|
+
| Direct lookup by name/id | mapped a name to one payload | maps a name to every payload, gated individually, first eligible wins |
|
|
841
|
+
|
|
842
|
+
The contract this restores is
|
|
843
|
+
[`placement-prioritization.md`](https://github.com/revt-eng/revturbine-devkit/blob/main/docs/specs/scaffold/placement-prioritization.md)
|
|
844
|
+
§1 stage 3 — "Targeting — the user's plan and segment match **a payload**" — with
|
|
845
|
+
§4 and Appendix D scoping drag precedence to payloads that are equally eligible.
|
|
846
|
+
|
|
847
|
+
**Why it mattered.** A placement holding an admin payload and a member payload
|
|
848
|
+
gave every member either the admin copy (when payload 1 was unchipped) or
|
|
849
|
+
nothing at all (when payload 1 was chipped to admins). An integration hit the
|
|
850
|
+
second shape on 0.7.13 and concluded config-driven segmentation was inert. Plan
|
|
851
|
+
233 TASK-7 had already made the chip predicate real — but the predicate was only
|
|
852
|
+
ever asked about one payload.
|
|
853
|
+
|
|
854
|
+
**Landed in.** 0.10.4 (all three ports, plus `@revt-eng/core` 0.1.326).
|
|
855
|
+
|
|
856
|
+
**Fail-closed in.** 0.10.4 — the same release. There is no tolerance window:
|
|
857
|
+
a user who matches no payload now receives `visible: false` with
|
|
858
|
+
`segment_target_mismatch` rather than payload 1's content.
|
|
859
|
+
|
|
860
|
+
**Proving test.** `tests/parity/fixtures/segment_chip_payload_selection.json`
|
|
861
|
+
(cross-language), plus `local-resolver-payload-selection.test.ts`,
|
|
862
|
+
`server-python/tests/placements/test_payload_selection.py` and
|
|
863
|
+
`server-rust/tests/payload_selection.rs` — each exercising **both** the slot and
|
|
864
|
+
the direct-lookup path, because gating one and not the other is exactly the back
|
|
865
|
+
door plans 138 and 233 each had to close.
|
|
866
|
+
|
|
867
|
+
**Who is affected.** Any Playbook authoring more than one payload on a
|
|
868
|
+
placement where payload 1 is not the payload a given user matches. Those users
|
|
869
|
+
now see the payload their chips select — which is the fix, and is a live
|
|
870
|
+
behaviour change for such configs. Single-payload placements are unaffected.
|
|
871
|
+
|
|
872
|
+
---
|
|
873
|
+
|
|
874
|
+
## 0.10.3
|
|
875
|
+
|
|
876
|
+
### The Rust port gains the decision surfaces Python already shipped (BL-0145)
|
|
877
|
+
|
|
878
|
+
**What changed.** `RevTurbineCustomerSdk` on the Rust port (`revturbine`,
|
|
879
|
+
crates.io) now exposes the same decision surfaces the Python port exposes.
|
|
880
|
+
Nothing on the TypeScript or Python side changes; this is a Rust-only,
|
|
881
|
+
purely additive release.
|
|
882
|
+
|
|
883
|
+
| Added | Shape |
|
|
884
|
+
|---|---|
|
|
885
|
+
| `RevTurbineCustomerSdk::can(handle, context)` | The advertised alias of `check_entitlement`, matching the scaffold SDK function surface (canonical `checkEntitlement`, alias `can`). |
|
|
886
|
+
| `RevTurbineCustomerSdk::get_eligible_plans()` | `Vec<EligiblePlan>` — public, segment-eligible plan variations. |
|
|
887
|
+
| `RevTurbineCustomerSdk::get_eligible_addons()` | `Vec<EligibleAddon>` — the add-on twin. |
|
|
888
|
+
| `RevTurbineCustomerSdk::evaluate_trial_status(instances, now_iso, base_plan_handle, usage_balances)` | `TrialEvaluation`, reading `free_trial_rules` / `reverse_trial_rules` from the constructed Playbook. |
|
|
889
|
+
| `revturbine::format_currency_minor_units(amount, currency, locale)` | Free function, because Python exposes it as a module function. |
|
|
890
|
+
| `revturbine::plans` | New module carrying the catalog eligibility port and the formatter. |
|
|
891
|
+
| `UserContext::segment_ids` | Pre-resolved segment ids the catalog methods match against. |
|
|
892
|
+
|
|
893
|
+
`UserContext` gains a field but derives `Default`, so existing construction
|
|
894
|
+
with `..Default::default()` keeps compiling.
|
|
895
|
+
|
|
896
|
+
**Why it was invisible.** The cross-language parity gate was green on the
|
|
897
|
+
catalog and currency scenarios only because the Rust parity runner
|
|
898
|
+
*reimplemented* `eligible_catalog` and `format_currency_minor_units` inline
|
|
899
|
+
rather than calling the crate — so the comparison never touched shipped code,
|
|
900
|
+
and the crate could lack the capability entirely without a single fixture
|
|
901
|
+
going red. The runner now calls `revturbine::plans`, and the inline copies are
|
|
902
|
+
deleted; all 102 comparisons stay byte-identical.
|
|
903
|
+
|
|
904
|
+
---
|
|
905
|
+
|
|
906
|
+
## 0.10.2
|
|
907
|
+
|
|
908
|
+
### Local mode resolves placements by slot id (BL-0119)
|
|
909
|
+
|
|
910
|
+
**What changed.** In `local_only` mode the browser SDK now resolves a placement
|
|
911
|
+
request against the Playbook's authored `placement_slots[]` registry.
|
|
912
|
+
`getPlacement({ slotId })` — with or without a `componentType` — previously
|
|
913
|
+
returned `null` for a slot the Playbook declared, and a controller/`usePlacement`
|
|
914
|
+
mount of that slot resolved to `placement_not_found`. Looking the same placement
|
|
915
|
+
up by **name** worked, which is what made this look like a config problem rather
|
|
916
|
+
than an SDK one.
|
|
917
|
+
|
|
918
|
+
Two things were missing, and both are now in place:
|
|
919
|
+
|
|
920
|
+
- **`registerSurfaceSlot()` adopts the declared template.** `surface_template_ids`
|
|
921
|
+
on the registered record is what puts the shared resolver on its slot branch,
|
|
922
|
+
where `trigger.slot_id` is matched. Without it the resolver fell back to direct
|
|
923
|
+
lookup, which is keyed by placement **name**/id and therefore can never match a
|
|
924
|
+
slot id — hence `placement_not_found`. An id passed explicitly at the call site
|
|
925
|
+
still wins; the mounting code knows what it renders.
|
|
926
|
+
- **`getPlacement()` falls through to the slot registry.** The local placement
|
|
927
|
+
cache only holds slots a decision has already run through, so a cold lookup
|
|
928
|
+
found nothing and stopped. It now derives the record from `placement_slots`,
|
|
929
|
+
matching on `id` and falling back to `surface_type` when only a component type
|
|
930
|
+
is given — the same derivation scaffold's headless
|
|
931
|
+
`LocalRuntime.slotRecordForConfig` has always performed. Registration on this
|
|
932
|
+
path is local only: reading a placement never writes a surface slot back to the
|
|
933
|
+
control plane.
|
|
934
|
+
|
|
935
|
+
This closes a browser-versus-headless divergence: the same Playbook decided
|
|
936
|
+
differently depending on which runtime read it. No public API change — no
|
|
937
|
+
signature moved, and nothing new is asked of the caller. A slot id that no
|
|
938
|
+
`placement_slots` entry declares still resolves to `null`, unchanged.
|
|
939
|
+
|
|
940
|
+
**Landed in** `0.10.2`. **Fail-closed in** n/a — a previously-`null` lookup now
|
|
941
|
+
returns the placement the Playbook authored.
|
|
942
|
+
|
|
943
|
+
**Proving test:** `web-sdk/local-slot-id-lookup.test.ts` — by-slot-id and
|
|
944
|
+
by-slot-id-plus-component-type both resolve, the controller path no longer
|
|
945
|
+
reports `placement_not_found`, an undeclared slot id still returns `null` and
|
|
946
|
+
gains no invented template, an explicit `surfaceTemplateIds` still wins, and the
|
|
947
|
+
by-name lookup that always worked keeps working (the positive control).
|
|
948
|
+
|
|
949
|
+
---
|
|
950
|
+
|
|
951
|
+
## 0.10.1
|
|
952
|
+
|
|
953
|
+
### Treatment interactions carry the identified `account_id`
|
|
954
|
+
|
|
955
|
+
**What changed.** Every treatment interaction the SDK sends to
|
|
956
|
+
`/api/events/interactions` — the impression that writes a
|
|
957
|
+
`placement_presentations` row, plus dismiss / click / conversion — now carries
|
|
958
|
+
`account_id`, taken from the account the integration identified
|
|
959
|
+
(`identify(userId, { account_id })` / `setUserContext`). It was never sent at
|
|
960
|
+
all, so the ingest route's `account_id ?? user_id` fallback stamped a **user**
|
|
961
|
+
id into an **account** column. `placement_presentations.account_id` is a join
|
|
962
|
+
key, not a label: `monetization_funnel` matches it against account ids from
|
|
963
|
+
`events_clickstream` / `events_billing`, and every experiment summary pipe
|
|
964
|
+
reads it when `analysis_unit='account'`. Those joins therefore matched only
|
|
965
|
+
where a user id happened to equal an account id — and an account-grain
|
|
966
|
+
experiment readout returned the user-grain n while looking perfectly valid.
|
|
967
|
+
|
|
968
|
+
Two details make it a usable key. The value is PII-redacted **identically to
|
|
969
|
+
`/api/track`**, so an email-shaped account id becomes the same hash in
|
|
970
|
+
`placement_presentations` and in `events_clickstream` and the funnel actually
|
|
971
|
+
joins. And when no account was identified the field is **omitted**, never set
|
|
972
|
+
to the user id — the route keeps its own fallback for SDKs that have not
|
|
973
|
+
upgraded, but a wrong join key is worse than a missing one.
|
|
974
|
+
|
|
975
|
+
No public API change: nothing new is asked of the caller, and
|
|
976
|
+
`RevTurbineTreatmentInteractionInput` is unchanged. Integrations that already
|
|
977
|
+
pass `account_id` to `identify()` get correct account-grain analytics with no
|
|
978
|
+
code change; integrations that never identify an account are unaffected on the
|
|
979
|
+
wire and see the same route-side fallback as before.
|
|
980
|
+
|
|
981
|
+
**Landed in** `0.10.1`. **Fail-closed in** n/a — additive; nothing old is
|
|
982
|
+
rejected.
|
|
983
|
+
|
|
984
|
+
**Proving test:** `web-sdk/interaction-wire-contract.test.ts` — "the account
|
|
985
|
+
identity the analytics joins key on": the identified account reaches the wire
|
|
986
|
+
distinct from `user_id`, an unidentified one is omitted rather than copied from
|
|
987
|
+
`user_id`, an email-shaped account id matches the `/api/track` lane
|
|
988
|
+
byte-for-byte, and a re-queued batch keeps the account that was acting when
|
|
989
|
+
each interaction happened.
|
|
990
|
+
|
|
991
|
+
---
|
|
992
|
+
|
|
993
|
+
## 0.10.0
|
|
994
|
+
|
|
995
|
+
### `publicKey` is the browser credential; `apiKey` is the server key
|
|
996
|
+
|
|
997
|
+
**What changed.** The browser init option for the ingest (public) key is
|
|
998
|
+
`publicKey` — on `initRevTurbine`, `<RevTurbineProvider options>`, and the
|
|
999
|
+
headless init alike. Every browser bearer (ingest, launched-Playbook delivery,
|
|
1000
|
+
user context, branding, trial status) uses that one key. `apiKey` now means
|
|
1001
|
+
one thing everywhere: the secret **server key**, as it always did on
|
|
1002
|
+
`@revturbine/sdk/server` — pass it from backend code only (a route handler
|
|
1003
|
+
using `@revturbine/sdk/headless` keeps using `apiKey`). The name states what the
|
|
1004
|
+
credential is at the call site: a security scanner or an agent that meets
|
|
1005
|
+
`publicKey` in a bundle knows it belongs there, where `apiKey` reads as a leak.
|
|
1006
|
+
|
|
1007
|
+
`apiKey` and `ingestPublicKey` are still accepted on a browser init as aliases
|
|
1008
|
+
of `publicKey` for one minor (precedence: `publicKey`, then `ingestPublicKey`,
|
|
1009
|
+
then `apiKey`). Using an alias without `publicKey` in a browser logs a one-time
|
|
1010
|
+
development warning naming `publicKey`; production builds are silent. On the
|
|
1011
|
+
browser `RevTurbineInitOptions` type, `apiKey` is now optional (it was required)
|
|
1012
|
+
and `ingestPublicKey` is `@deprecated`. Keyless local-only init is unchanged,
|
|
1013
|
+
and the plan 95 anonymous `sdk_init` beacon still fires only when no public key
|
|
1014
|
+
(`publicKey` or `ingestPublicKey`) was supplied.
|
|
1015
|
+
|
|
1016
|
+
Ruling: Kent, 2026-09-21, devkit PR #808 (closed).
|
|
1017
|
+
|
|
1018
|
+
**Landed in** `0.10.0`. **Fail-closed in** `0.11.0` — `ingestPublicKey` was
|
|
1019
|
+
removed from the options type and `apiKey` stopped being read as a browser
|
|
1020
|
+
credential (BL-0113); see the `0.11.0` entry above.
|
|
1021
|
+
|
|
1022
|
+
**Proving test:** `web-sdk/public-key-option.test.ts` — precedence, the single
|
|
1023
|
+
bearer across ingest and control-plane fetches, the browser-only warning, and
|
|
1024
|
+
the keyless local-only init. Type-level: `web-sdk/init-options-exactness.test-d.ts`
|
|
1025
|
+
accepts `publicKey` and rejects the `publickey` casing typo.
|
|
1026
|
+
|
|
1027
|
+
### `endpoint` and `mode` are optional on `initRevTurbine` and `<RevTurbineProvider>`
|
|
1028
|
+
|
|
1029
|
+
**What changed.** `RevTurbineInitOptions.endpoint` defaults to
|
|
1030
|
+
`https://revturbine.com/app` (`DEFAULT_HOSTED_ENDPOINT`, exported) and
|
|
1031
|
+
`RevTurbineInitOptions.mode` defaults to `'snippet'` (`DEFAULT_SDK_MODE`,
|
|
1032
|
+
exported); `<RevTurbineProvider>` passes `'react'` when the app does not set it.
|
|
1033
|
+
Both were required; `mode` only ever labelled telemetry (`page_view.mode` and a
|
|
1034
|
+
diagnostics message), and every hosted integration used the same endpoint.
|
|
1035
|
+
Existing calls that pass them are unchanged. Local mode is unchanged (its
|
|
1036
|
+
placeholder defaults already applied). Additive, no version bump required.
|
|
1037
|
+
|
|
1038
|
+
**Landed in** `0.10.0`. **Fail-closed in** n/a — nothing old is rejected.
|
|
1039
|
+
|
|
1040
|
+
**Proving test:** `web-sdk/public-init.test.ts` — "defaults endpoint and mode
|
|
1041
|
+
for a hosted init that omits them".
|
|
1042
|
+
|
|
1043
|
+
### Reason-code compatibility verification
|
|
1044
|
+
|
|
1045
|
+
Added a reviewed baseline for 27 entitlement and 21 placement reason values,
|
|
1046
|
+
checked against live SDK/core fixtures. Removal/rename controls exercise every
|
|
1047
|
+
protected value, and baseline changes require this changelog to change through
|
|
1048
|
+
the existing public-API gate. Custom provider reasons remain extensible and
|
|
1049
|
+
free-form diagnostics are excluded. This adds verification only; no runtime
|
|
1050
|
+
reason values or package versions change.
|
|
1051
|
+
|
|
1052
|
+
Proving tests: `web-sdk/reason-contract.test.ts` and
|
|
1053
|
+
`web-sdk/reason-contract-policy.test.ts`. The intentional-change procedure is
|
|
1054
|
+
in `docs/reason-code-contract.md`.
|
|
1055
|
+
|
|
1056
|
+
## 0.9.3
|
|
1057
|
+
|
|
1058
|
+
### Fixed
|
|
1059
|
+
|
|
1060
|
+
- Placement explanations use the same published segment-eligibility helper as
|
|
1061
|
+
decisions, keeping OR-within-segments and AND-with-plan behavior aligned.
|
|
1062
|
+
The explanation/decision matrix covers local and provider-backed Playbooks,
|
|
1063
|
+
unknown or missing segments, exact handle matching and plan-filter combinations.
|
|
1064
|
+
|
|
1065
|
+
## 0.9.2
|
|
1066
|
+
|
|
1067
|
+
### Public initializer user context
|
|
1068
|
+
|
|
1069
|
+
`await initRevTurbine({ user: { id, ...context }, ...options })` from
|
|
1070
|
+
`@revturbine/sdk` or `@revturbine/sdk/headless` now passes `id` only as the
|
|
1071
|
+
identity argument. Supported user context retains its existing behavior without
|
|
1072
|
+
the spurious unrecognized-context-key warning for `id`; genuinely unknown keys
|
|
1073
|
+
still warn. Public quick starts now show the awaited `SdkSession` and
|
|
1074
|
+
`session.sdk.getBranding()`. The separate synchronous core initializer is unchanged.
|
|
1075
|
+
|
|
1076
|
+
Proving tests: `web-sdk/public-init.test.ts` and the installed public-package
|
|
1077
|
+
runtime/type examples in `web-sdk/scripts/check-public-diagnostics.mjs`.
|
|
1078
|
+
|
|
1079
|
+
## 0.9.1
|
|
1080
|
+
|
|
1081
|
+
### Public changelog distribution
|
|
1082
|
+
|
|
1083
|
+
The maintained changelog is now included as `CHANGELOG.md` in the npm package
|
|
1084
|
+
and mirrored at the root of the public SDK repository. The docs Reference
|
|
1085
|
+
navigation links directly to that authoritative public file. The independent,
|
|
1086
|
+
hidden docs copy has been retired. The pre-0.3.0 history gap below remains;
|
|
1087
|
+
this release does not reconstruct it. No SDK runtime behavior changed.
|
|
1088
|
+
|
|
1089
|
+
## 0.9.0
|
|
1090
|
+
|
|
1091
|
+
### Placement eligibility follows current context after conversion
|
|
1092
|
+
|
|
1093
|
+
- **What changed** — Conversion remains an analytics event and no longer retires
|
|
1094
|
+
or temporarily suppresses a placement. Legacy conversion-owned browser state
|
|
1095
|
+
is ignored. Supply the updated UserContext after a plan change; Playbook
|
|
1096
|
+
targeting determines whether the offer still applies.
|
|
1097
|
+
- **What changed** — Fixed and Access Gate placements bypass dismissal, reminder
|
|
1098
|
+
and bare-click cooldowns. Dismiss and snooze close the current display; the
|
|
1099
|
+
next explicit trigger, refresh or mount evaluates again. This uses the resolved
|
|
1100
|
+
placement category, including when another category renders inside a Fixed slot.
|
|
1101
|
+
Other categories retain authored/default windows, and explicit system
|
|
1102
|
+
suppression and entitlement denial remain effective.
|
|
1103
|
+
- **Landed in** — `0.9.0` across JavaScript, Python and Rust.
|
|
1104
|
+
- **Fail-closed in** — Not applicable; no input shape was removed. Rust callers
|
|
1105
|
+
constructing `InteractionState` literals should supply the new optional
|
|
1106
|
+
`explicit_suppressed_until` field or use `..Default::default()`.
|
|
1107
|
+
- **Proving test** — `web-sdk/conversion-eligibility.test.ts`,
|
|
1108
|
+
`web-sdk/placements/FixedSurfaceSlot.dismissal.test.tsx`, and the shared
|
|
1109
|
+
`placement_category_interactions` parity fixture.
|
|
1110
|
+
|
|
1111
|
+
## 0.8.9
|
|
1112
|
+
|
|
1113
|
+
### JavaScript: development diagnostics survive package minification
|
|
1114
|
+
|
|
1115
|
+
- **What changed** — The SDK package preserves the environment check until the
|
|
1116
|
+
consuming application builds it. Previously the minified package baked in
|
|
1117
|
+
production mode, hiding the React initialization-failure banner, app-theme
|
|
1118
|
+
override warning and legacy-theme warning even in development applications.
|
|
1119
|
+
Production applications still suppress those development diagnostics;
|
|
1120
|
+
`initStatus` and required initialization-error reporting remain available.
|
|
1121
|
+
The existing fallback for browsers without `process` is preserved.
|
|
1122
|
+
- **Landed in** — `0.8.9`.
|
|
1123
|
+
- **Fail-closed in** — Not applicable; no decision or initialization policy changes.
|
|
1124
|
+
- **Proving test** — `web-sdk/scripts/check-public-diagnostics.mjs` installs the
|
|
1125
|
+
public tarball and runs Chromium against separate development, production
|
|
1126
|
+
and environment-preserving consumer bundles. The PR release-build gate
|
|
1127
|
+
retains the tarball, inventory, SHA-256 and observed diagnostics.
|
|
1128
|
+
|
|
1129
|
+
Python and Rust receive the matching version bump; their behavior is unchanged.
|
|
1130
|
+
|
|
1131
|
+
## 0.8.8
|
|
1132
|
+
|
|
1133
|
+
Behavioural fixes in Python; a new decision surface in Rust.
|
|
1134
|
+
|
|
1135
|
+
### Python: numeric coercion unified onto JS Number() semantics
|
|
1136
|
+
|
|
1137
|
+
- **What changed** — `parse_numberish`'s string branch used Python
|
|
1138
|
+
`float()`, which accepts forms JS `Number()` rejects (underscore
|
|
1139
|
+
separators: `"1_000"` scored 1000 here, NaN->0 in TypeScript — a
|
|
1140
|
+
candidate-flipping divergence in every selection scorer) and rejects
|
|
1141
|
+
forms JS accepts (`"0x10"` is 16). The one JS-semantics parser from the
|
|
1142
|
+
plan-233 segments port is promoted to `helpers.js_number` and every
|
|
1143
|
+
coercion site shares it. Ripple alignments: `Number("")` is 0 (the old
|
|
1144
|
+
is-None assertion pinned the float() behaviour), a whitespace-only
|
|
1145
|
+
`template_version` resolves to `"0"` exactly as TS does, and
|
|
1146
|
+
`superseded_versions` no longer fabricates a `"0"` entry from an empty
|
|
1147
|
+
string (mirrors TS's trim-then-filter).
|
|
1148
|
+
|
|
1149
|
+
### Rust: the candidate-selection layer, ported
|
|
1150
|
+
|
|
1151
|
+
- **What changed** — `resolve_local_placement_from_candidates` plus the
|
|
1152
|
+
full helper set (category buckets, plan-53 two-stage tier-3 urgency,
|
|
1153
|
+
milestone supersession, category conflict suppression, server-order
|
|
1154
|
+
dominance) now exists in Rust (`placements/selection.rs`). Until now the
|
|
1155
|
+
Rust port selected by entry order only; competing-category selections
|
|
1156
|
+
had no third side, which is why plan 234 TASK-15's fixture was deferred.
|
|
1157
|
+
- **Proving test** — parity fixture `placement_selection_priority`
|
|
1158
|
+
(10 calls: cross-tier dominance, both tier-3 stages including a
|
|
1159
|
+
class-vs-proximity disagreement, retention/conversion tie, server_order
|
|
1160
|
+
skipping conflict suppression, fixed_only, milestone supersession, both
|
|
1161
|
+
REQ-5 coercion lanes, output_id tiebreak), byte-identical ts/py/rs on
|
|
1162
|
+
its first regen, plus mirrored port-local suites in Python and Rust.
|
|
1163
|
+
|
|
1164
|
+
## 0.8.7
|
|
1165
|
+
|
|
1166
|
+
No runtime behaviour change in any port; the release carries new parity
|
|
1167
|
+
locks and one serde derive.
|
|
1168
|
+
|
|
1169
|
+
### All ports: trial-status derivation and allocation scoping parity-locked
|
|
1170
|
+
|
|
1171
|
+
- **What changed** — two decision surfaces every runtime already evaluated
|
|
1172
|
+
gained their first cross-language locks. `evaluateTrialStatus` (the
|
|
1173
|
+
free/reverse trial-rule derivation): the TS golden vectors mirrored the
|
|
1174
|
+
Python unit suite by hand-maintained convention, and Rust was never in
|
|
1175
|
+
the mirror at all; the `trial_status_evaluation` fixture makes the
|
|
1176
|
+
agreement a byte-checked property. `computeConsumedPercent` (the spec
|
|
1177
|
+
3.4 Pooling allocation-scoped grant selection): reachable from no
|
|
1178
|
+
placement fixture because the static provider has no grants lane; the
|
|
1179
|
+
`threshold_allocation_scoping` fixture drives every allocation label
|
|
1180
|
+
against a hierarchy whose levels carry DIFFERENT counters.
|
|
1181
|
+
- **A wire divergence caught on first regen** — the reverse-trial grants
|
|
1182
|
+
set serialized three different ways (TS `JSON.stringify(Set)` = `{}`
|
|
1183
|
+
with the contents silently lost; Python's canonicalizer dropped the key;
|
|
1184
|
+
Rust emitted the honest list). The runners now own the wire form: a
|
|
1185
|
+
sorted array on every side.
|
|
1186
|
+
- **Fail-closed in** — no evaluator changed; `TrialEvaluation` gained a
|
|
1187
|
+
`Serialize` derive in Rust (additive).
|
|
1188
|
+
- **Proving test** — parity fixtures `trial_status_evaluation` (6 calls;
|
|
1189
|
+
the usage-lane call was sharpened after the first cut put
|
|
1190
|
+
`trial_limit_type` on the RULE and every side agreed on `time` - the
|
|
1191
|
+
non-discriminating shape REQ-3 forbids; it rides the INSTANCE) and
|
|
1192
|
+
`threshold_allocation_scoping` (9 calls).
|
|
1193
|
+
|
|
1194
|
+
## 0.8.6
|
|
1195
|
+
|
|
1196
|
+
One capability lands in the two ports; TypeScript is unchanged and ships this
|
|
1197
|
+
version for lockstep alone.
|
|
1198
|
+
|
|
1199
|
+
### Python + Rust: targeting-state derivation ported (buildTargetingState)
|
|
1200
|
+
|
|
1201
|
+
- **What changed** — `LocalRuntime.build_targeting_state` raised
|
|
1202
|
+
NotImplementedError in Python (a REQ-14 deferral) and had no Rust port at
|
|
1203
|
+
all, so the derivation from a raw user context to the traits segment
|
|
1204
|
+
evaluation consumes existed only in TypeScript: segment parity proved
|
|
1205
|
+
"agrees given the same traits", never "agrees given the same user
|
|
1206
|
+
context". Both ports now carry the pure derivation
|
|
1207
|
+
(`core.user_context.build_targeting_state` / `user_context.rs`), byte-
|
|
1208
|
+
locked against the TS canonical.
|
|
1209
|
+
- **Also aligned red-first** — Python's dormant
|
|
1210
|
+
`configured_plan_name_from_exported_config` (no callers until now) still
|
|
1211
|
+
carried pre-plan-120 semantics: id matching, plan-object resolution, and
|
|
1212
|
+
a `_handle` suffix fallback. Plans resolve by `unique_handle` alone; the
|
|
1213
|
+
drift-pinning tests were corrected with the alignment.
|
|
1214
|
+
- **Fail-closed in** — identity rules are shadow-proof by construction:
|
|
1215
|
+
`plan_handle` is a reserved trait a `custom.plan_handle` can never
|
|
1216
|
+
impersonate, and no identity means the reserved trait is REMOVED and the
|
|
1217
|
+
effective plan omitted.
|
|
1218
|
+
- **Proving test** — parity fixture `targeting_state_derivation` (6 calls:
|
|
1219
|
+
identity precedence, shadow-proofing, plan-OBJECT fallback, the id that
|
|
1220
|
+
must resolve nothing, the no-identity removal, override-wins usage with a
|
|
1221
|
+
legacy bare-number entry), byte-identical ts/py/rs, plus the corrected
|
|
1222
|
+
unit matrices in both ports.
|
|
1223
|
+
|
|
1224
|
+
## 0.8.5
|
|
1225
|
+
|
|
1226
|
+
One behavioural alignment, Python only. TypeScript and Rust are unchanged and
|
|
1227
|
+
ship this version for lockstep alone.
|
|
1228
|
+
|
|
1229
|
+
### Python: category buckets and tier-3 urgency aligned to the plan-53 model
|
|
1230
|
+
|
|
1231
|
+
- **What changed** — this port's `category_bucket` carried the pre-plan-53
|
|
1232
|
+
map for months (trial=3, retention=5 where the canonical folds trial into
|
|
1233
|
+
priority tier 3 as bucket 2 and retention into the single discretionary
|
|
1234
|
+
bucket 4); its docstring cited source lines that had moved AND changed
|
|
1235
|
+
upstream. The map feeds the selection comparator, so a trial and a usage
|
|
1236
|
+
placement competing for one surface ordered STRICTLY here (usage always
|
|
1237
|
+
first) while the canonical ties them and applies the plan-53 two-stage
|
|
1238
|
+
urgency model — which this port also lacked. `tier3_class` is now ported
|
|
1239
|
+
and staged ahead of proximity in `resolve_local_placement_from_candidates`,
|
|
1240
|
+
the map matches the canonical, and the system-caps exemption boundary moved
|
|
1241
|
+
from the compensating `<= 3` to the canonical `<= 2` (the exempted category
|
|
1242
|
+
set is unchanged).
|
|
1243
|
+
- **Landed in** — `helpers.category_bucket` / `helpers.tier3_class`, the
|
|
1244
|
+
comparator in `placement_decision.py`, and the caps exemption boundary.
|
|
1245
|
+
- **Fail-closed in** — no gate changes; this is ordering. Exemptions and
|
|
1246
|
+
discretionary classification are set-identical before and after.
|
|
1247
|
+
- **Proving test** — `tests/placements/test_selection_tier3.py` (class
|
|
1248
|
+
ordering with class-vs-proximity DISAGREEMENT cases; mutation-checked:
|
|
1249
|
+
disabling the tier3 stage fails exactly the two stage-1 tests), plus the
|
|
1250
|
+
bucket-map matrix in `test_helpers.py` whose old strict `usage < trial`
|
|
1251
|
+
invariant was pinning the drift. The cross-language fixture for this
|
|
1252
|
+
surface lands with the Rust selection-layer port (plan 234 TASK-8).
|
|
1253
|
+
|
|
1254
|
+
## 0.8.4
|
|
1255
|
+
|
|
1256
|
+
One behavioural alignment, Python only. TypeScript and Rust are unchanged and
|
|
1257
|
+
ship this version for lockstep alone.
|
|
1258
|
+
|
|
1259
|
+
### Python: cap/cooldown maths consolidated onto the shared primitive
|
|
1260
|
+
|
|
1261
|
+
- **What changed** — `placement_decision.py` and `cap_enforcer.py` carried
|
|
1262
|
+
their own copies of the cap-window and cooldown arithmetic that TypeScript
|
|
1263
|
+
consolidated onto `evaluateCaps` (plan 233 TASK-15 / scaffold #351). Both
|
|
1264
|
+
now delegate to the new `cap_rules.evaluate_caps` port. Reading the bodies
|
|
1265
|
+
side by side surfaced two silent divergences, both aligned to the TS
|
|
1266
|
+
canonical: Fixed / Access-Gate categories are now exempt from per-payload
|
|
1267
|
+
caps in `check_placement_caps` (TS has exempted them since plan 167; this
|
|
1268
|
+
port capped them), and a cap deny now persists the UNFILTERED state — the
|
|
1269
|
+
old bodies trimmed `seen_at` to the tripping window (one comment claimed
|
|
1270
|
+
"Mirrors TS"; TS does no such trim), silently shrinking future week/month
|
|
1271
|
+
windows.
|
|
1272
|
+
- **Landed in** — `core/placements/cap_rules.py` (the primitive),
|
|
1273
|
+
`check_placement_caps` / `check_system_presentation_caps` /
|
|
1274
|
+
`CapEnforcer.enforce` (the delegations).
|
|
1275
|
+
- **Fail-closed in** — verdicts only ever tighten or stay: the exemption
|
|
1276
|
+
affects categories that are user-prompted by design, and the state
|
|
1277
|
+
alignment preserves more history, never less.
|
|
1278
|
+
- **Proving test** — `tests/placements/test_cap_delegation.py`: a 12-case
|
|
1279
|
+
characterization matrix green before AND after the refactor, the two
|
|
1280
|
+
aligned behaviours run RED against the pre-refactor body, and a mutation
|
|
1281
|
+
of the shared window arithmetic fails four tests across both entry points
|
|
1282
|
+
(the shared maths is reached, not shadowed).
|
|
1283
|
+
|
|
1284
|
+
## 0.8.3
|
|
1285
|
+
|
|
1286
|
+
One behavioural fix, Rust only. TypeScript and Python are unchanged and ship
|
|
1287
|
+
this version for lockstep alone.
|
|
1288
|
+
|
|
1289
|
+
### Rust: a confirmed conversion did not retire the placement
|
|
1290
|
+
|
|
1291
|
+
- **What changed** — the Rust port carried the full ImpressionHistory
|
|
1292
|
+
machinery (`record_conversion`, `is_hidden_sync`, the retired cache) since
|
|
1293
|
+
the port landed, but its placement resolver never consulted it. A user who
|
|
1294
|
+
completed a placement's CTA — a confirmed conversion — kept seeing that
|
|
1295
|
+
placement in this port alone, while TypeScript core
|
|
1296
|
+
(`local-resolver.ts:610-612`, `:726`) and Python both hide it permanently.
|
|
1297
|
+
This is the defect class plan 167's unshipped follow-up became as a
|
|
1298
|
+
customer P1: paying customers shown the upsell they already bought.
|
|
1299
|
+
- **Landed in** — the retirement gate is wired at both decision lanes: the
|
|
1300
|
+
candidate filter (before the trigger gates, mirroring the TS order) and
|
|
1301
|
+
the direct-lookup path, which now answers `placement_retired`, keyed by
|
|
1302
|
+
the output's `rule_id` exactly as TS core and Python key it.
|
|
1303
|
+
- **Fail-closed in** — the gate only ever hides; an empty or cold history
|
|
1304
|
+
changes nothing, and a placement with no recorded conversion is untouched.
|
|
1305
|
+
- **Proving test** — parity fixture `placement_conversion_retires`
|
|
1306
|
+
(visible -> convert -> `placement_retired`, with an untouched control
|
|
1307
|
+
staying visible, byte-identical across ts/py/rs; mutation-checked: making
|
|
1308
|
+
the Rust gate inert diverges the snapshot) and
|
|
1309
|
+
`server-rust/tests/local_runtime.rs`
|
|
1310
|
+
(`a_confirmed_conversion_permanently_retires_the_placement`).
|
|
1311
|
+
|
|
1312
|
+
## 0.8.2
|
|
1313
|
+
|
|
1314
|
+
One behavioural fix, Rust only. TypeScript and Python are unchanged and ship
|
|
1315
|
+
this version for lockstep alone.
|
|
1316
|
+
|
|
1317
|
+
### Rust: provider-path rule selection was source-order, not most-permissive
|
|
1318
|
+
|
|
1319
|
+
- **What changed** — on the provider path (`check_entitlement` with a rules
|
|
1320
|
+
provider, the path a static config registers), the Rust port's §2.6.5
|
|
1321
|
+
most-permissive selection scored each rule snapshot's root object instead
|
|
1322
|
+
of its `fields` sub-object, where the adapter actually nests the score keys
|
|
1323
|
+
(`limit_value`, `allowance`, `included_count`, `enabled`). Every rule
|
|
1324
|
+
scored 0, so the deterministic tie-break — earliest in source order —
|
|
1325
|
+
silently decided instead. Concretely: an entitlement carrying both a seat
|
|
1326
|
+
rule (listed first) and a `usage_limit` rule at limit 2 returned **allowed**
|
|
1327
|
+
at `used: 3` where TypeScript and Python return limited
|
|
1328
|
+
(`usage_limit_reached`). Whether you were affected depends on rule order in
|
|
1329
|
+
your Playbook, which is what made this silent. The ExportedConfig fallback
|
|
1330
|
+
path (`derive_local_entitlement_from_configured_rules`) was NOT affected —
|
|
1331
|
+
it passes the merged type-fields bag and scored correctly.
|
|
1332
|
+
- **Landed in** — `0.8.2`.
|
|
1333
|
+
- **Fail-closed in** — `0.8.2` (same release; no tolerated window).
|
|
1334
|
+
- **Proving test** — parity fixture `entitlement_rule_seat_included_count`
|
|
1335
|
+
(byte-locked ts ≡ py ≡ rs; diverged on exactly the shadowed-limit call until
|
|
1336
|
+
the fix) and `most_permissive_scores_the_fields_sub_object_not_the_snapshot_root`
|
|
1337
|
+
(Rust unit, mutation-checked). The pre-existing Rust unit test had authored
|
|
1338
|
+
its score key at the snapshot root — a shape the adapter never emits — and
|
|
1339
|
+
passed over the bug; it now uses the production shape.
|
|
1340
|
+
|
|
1341
|
+
---
|
|
1342
|
+
|
|
1343
|
+
## 0.8.1
|
|
1344
|
+
|
|
1345
|
+
One behavioural fix, Python and Rust only. The TypeScript SDK is unchanged and
|
|
1346
|
+
ships this version for lockstep alone.
|
|
1347
|
+
|
|
1348
|
+
### Python/Rust: cross-dimension segment AND no longer degrades to flat OR
|
|
1349
|
+
|
|
1350
|
+
- **What changed** — the config evaluator's segment→dimension lookup was keyed
|
|
1351
|
+
by the segment's `id` in the Python and Rust ports, where the TypeScript
|
|
1352
|
+
canonical keys it by `handle`. Rule `segment_ids` are handle-valued, so on
|
|
1353
|
+
any Playbook whose segment ids differ from their handles — every real
|
|
1354
|
+
export — the ports' lookups all missed, every rule segment collapsed into
|
|
1355
|
+
the uncategorised bucket, and a rule spanning two dimensions (which must
|
|
1356
|
+
require BOTH, intra-dimension OR + cross-dimension AND) matched on EITHER.
|
|
1357
|
+
Concretely: an entitlement rule scoped to `emea` (region) AND `admins`
|
|
1358
|
+
(role) **granted** a user who was only in `emea`. It now denies, matching
|
|
1359
|
+
the TypeScript SDK. If your integration passes a non-empty user segment set
|
|
1360
|
+
to `derive_local_entitlement_from_configured_rules` and relies on a
|
|
1361
|
+
cross-dimension rule, users who matched only one dimension lose the grant
|
|
1362
|
+
on upgrade — that is the fix working. The built-in runtime fallback path
|
|
1363
|
+
passes an empty segment set and is unaffected.
|
|
1364
|
+
- **Landed in** — `0.8.1`.
|
|
1365
|
+
- **Fail-closed in** — `0.8.1` (same release; no tolerated window).
|
|
1366
|
+
- **Proving test** — parity fixtures `entitlement_segment_dimensions` /
|
|
1367
|
+
`entitlement_segment_no_dim_bucket` (byte-locked TS ≡ Python ≡ Rust; the
|
|
1368
|
+
regen diverged on exactly the cross-dimension calls until the fix), plus
|
|
1369
|
+
`TestSegmentDimensionLookup` (Python) and `cross_dimension_and_*` (Rust).
|
|
1370
|
+
|
|
1371
|
+
## 0.8.0
|
|
1372
|
+
|
|
1373
|
+
Plan 233 remediated four escalated defect families, and this is the release that
|
|
1374
|
+
delivers them. Everything below sat merged on `main` and unpublished for the
|
|
1375
|
+
length of that plan — 20 commits, twelve breaking changes — because the version
|
|
1376
|
+
never moved off `0.7.13`.
|
|
1377
|
+
|
|
1378
|
+
**Read this section before upgrading from `0.7.x`.** Three of these change live
|
|
1379
|
+
behaviour rather than only types: segment-targeted payloads stop showing to
|
|
1380
|
+
users outside the segment, `sdk.dismiss()` starts writing suppression where it
|
|
1381
|
+
previously did nothing, and a converted placement stops coming back. Each is the
|
|
1382
|
+
fix working, and each is visible to your users the moment you upgrade.
|
|
1383
|
+
|
|
1384
|
+
### The `tenantId` init option is now the authority over a Playbook's `tenant_id`
|
|
1385
|
+
|
|
1386
|
+
- **What changed** — the Playbook's `tenant_id` used to win, and the init option
|
|
1387
|
+
was a fallback applied only to legacy artifacts. That combination killed every
|
|
1388
|
+
integration that served a canonical Playbook *and* passed the tenant at init:
|
|
1389
|
+
the artifact had no `tenant_id` to win with, and the option was withheld
|
|
1390
|
+
because the artifact was canonical, so init threw. The init option now wins.
|
|
1391
|
+
The artifact's `tenant_id` is a **guard** — it exists so the CLI can refuse to
|
|
1392
|
+
upload a config to the wrong tenant — and on a mismatch the SDK warns, naming
|
|
1393
|
+
both values, and proceeds. It never fails init.
|
|
1394
|
+
- **Landed in** — `0.8.0`
|
|
1395
|
+
- **Fail-closed in** — n/a. This change makes init *succeed* where it used to
|
|
1396
|
+
throw. The old behaviour was the failure.
|
|
1397
|
+
- **Proving test** — `web-sdk/config-target-precedence-init.test.ts`
|
|
1398
|
+
|
|
1399
|
+
### Unrecognized and missing init options are compile errors
|
|
1400
|
+
|
|
1401
|
+
- **What changed** — init options are exact-checked. A typo (`apikey` for
|
|
1402
|
+
`apiKey`) or a stale option is a type error rather than a value the SDK
|
|
1403
|
+
silently ignores. This catches the case TypeScript's excess-property check
|
|
1404
|
+
never saw: options assembled in a variable, or in an un-annotated `useMemo`,
|
|
1405
|
+
which is exactly how React integrations build them.
|
|
1406
|
+
- **Landed in** — `0.8.0`
|
|
1407
|
+
- **Fail-closed in** — same. Unknown keys were ignored at runtime before and
|
|
1408
|
+
still are; what changed is that you cannot write them.
|
|
1409
|
+
- **Proving test** — `web-sdk/init-options-exactness.test-d.ts`
|
|
1410
|
+
|
|
1411
|
+
### A truncated Playbook is a compile error at the `localRuntime` boundary
|
|
1412
|
+
|
|
1413
|
+
- **What changed** — `UnvalidatedConfigArtifact` was `Record<string, unknown>`
|
|
1414
|
+
and now structurally requires the body arrays (`plans`, `entitlements`,
|
|
1415
|
+
`entitlement_rules`, `segments`, `content_ui_paths`). A Playbook missing one is
|
|
1416
|
+
now caught by `tsc` instead of throwing at init.
|
|
1417
|
+
- **Who this breaks** — only a caller who **explicitly annotated** their artifact
|
|
1418
|
+
`Record<string, unknown>`; that must now be narrowed. A JSON module import and
|
|
1419
|
+
a fetched artifact (`await res.json()`, typed `any`) both still compile with no
|
|
1420
|
+
cast.
|
|
1421
|
+
- **Landed in** — `0.8.0`
|
|
1422
|
+
- **Fail-closed in** — same version, at compile time. Runtime validation was
|
|
1423
|
+
already fail-closed and is unchanged.
|
|
1424
|
+
- **Proving test** — `web-sdk/playbook-structural.test-d.ts`
|
|
1425
|
+
|
|
1426
|
+
### Init failure is observable without an SDK instance
|
|
1427
|
+
|
|
1428
|
+
- **What changed** — when init fails the provider used to log and render children
|
|
1429
|
+
anyway, so the SDK simply never started and every diagnostic probe was
|
|
1430
|
+
unreachable *in precisely the failure mode that matters most*. `initStatus` is
|
|
1431
|
+
now on the React context and is readable when the SDK instance is `null`, with
|
|
1432
|
+
a non-empty `remediation` string on every init-path error.
|
|
1433
|
+
- **Landed in** — `0.8.0`
|
|
1434
|
+
- **Fail-closed in** — same.
|
|
1435
|
+
- **Proving test** — `web-sdk/react/RevTurbineProvider.init-status.test.tsx`
|
|
1436
|
+
|
|
1437
|
+
### The rendered theme resolves through the branding ladder
|
|
1438
|
+
|
|
1439
|
+
- **What changed** — `RevTurbineProvider` read `playbook?.theme` directly, so the
|
|
1440
|
+
`branding` init option never reached `useRevTurbineTheme()`. `getBranding()`
|
|
1441
|
+
and the rendered theme disagreed; that divergence *was* the defect. Both now
|
|
1442
|
+
resolve from the same rung.
|
|
1443
|
+
- **Landed in** — `0.8.0`
|
|
1444
|
+
- **Fail-closed in** — same.
|
|
1445
|
+
- **Proving test** — `web-sdk/react/RevTurbineProvider.branding-ladder.test.tsx`
|
|
1446
|
+
|
|
1447
|
+
### `RevTurbineThemeProvider` merges partial themes, and an app-mounted one wins
|
|
1448
|
+
|
|
1449
|
+
- **What changed** — a partial theme replaced the defaults wholesale instead of
|
|
1450
|
+
merging, so supplying one token dropped every other. Partial themes now merge.
|
|
1451
|
+
Separately: if your app mounts its own `RevTurbineThemeProvider`, it wins over
|
|
1452
|
+
the SDK's internal default, and a conflict warns in development.
|
|
1453
|
+
- **Landed in** — `0.8.0`
|
|
1454
|
+
- **Fail-closed in** — same.
|
|
1455
|
+
- **Proving test** — `web-sdk/theme/ThemeContext.partial.test.tsx`,
|
|
1456
|
+
`web-sdk/react/RevTurbineProvider.theme-nesting.test.tsx`
|
|
1457
|
+
|
|
1458
|
+
### Light/dark schemes no longer require re-initializing the SDK
|
|
1459
|
+
|
|
1460
|
+
- **What changed** — additive. A `colorScheme` option and `setColorScheme()` swap
|
|
1461
|
+
palettes while keeping SDK instance identity stable. The previous workaround
|
|
1462
|
+
was re-initializing the SDK, which discarded its state.
|
|
1463
|
+
- **Landed in** — `0.8.0`
|
|
1464
|
+
- **Fail-closed in** — n/a, additive.
|
|
1465
|
+
- **Proving test** — `web-sdk/react/RevTurbineProvider.color-scheme.test.tsx`
|
|
1466
|
+
|
|
1467
|
+
### `segment_chips` are enforced in the decision path
|
|
1468
|
+
|
|
1469
|
+
- **What changed** — payload `segment_chips` were evaluated by
|
|
1470
|
+
`explainPlacementDecision` but **not** by the decision path itself, so the
|
|
1471
|
+
probe and the decision disagreed and a segment-targeted payload could show to a
|
|
1472
|
+
user outside the segment. Both now call one shared helper, in every port.
|
|
1473
|
+
- **Who this breaks** — an integration relying, knowingly or not, on
|
|
1474
|
+
segment-targeted payloads showing to everyone will now see them correctly
|
|
1475
|
+
restricted. **There is no grace period**: this was ruled a bug, not a behaviour
|
|
1476
|
+
change.
|
|
1477
|
+
- **Landed in** — `0.8.0`
|
|
1478
|
+
- **Fail-closed in** — same version, and in all three runtimes together.
|
|
1479
|
+
- **Proving test** — `tests/parity/fixtures/segment_chip_targeting.json` and
|
|
1480
|
+
`tests/parity/fixtures/segment_evaluation.json`, run byte-identical across
|
|
1481
|
+
TypeScript, Python and Rust.
|
|
1482
|
+
|
|
1483
|
+
### Dismissal actually suppresses, at the authored window
|
|
1484
|
+
|
|
1485
|
+
- **What changed** — two defects. The interaction-state **read** omitted
|
|
1486
|
+
`treatmentId` while the **write** included it, and the controllers always
|
|
1487
|
+
populate it, so the keys could never match and dismissal suppression was dead
|
|
1488
|
+
for every React integration. And the window was a hardcoded 24-hour literal
|
|
1489
|
+
passed explicitly at every call site, which made both the authored
|
|
1490
|
+
`cooldown_after_dismiss_days` and the 7-day default unreachable. The window now
|
|
1491
|
+
resolves: explicit caller value → **authored config** → SDK default.
|
|
1492
|
+
`remind_later_minutes` resolves independently, so "remind me" no longer means
|
|
1493
|
+
"hide for a week".
|
|
1494
|
+
- **Who this breaks** — dismissal now suppresses where it previously did nothing.
|
|
1495
|
+
An integration that layered its own suppression on top may double-suppress.
|
|
1496
|
+
- **Landed in** — `0.8.0`
|
|
1497
|
+
- **Fail-closed in** — same. Note the *shape* of this one: it shipped in plan 167
|
|
1498
|
+
with a proving test that dismissed **without** a `treatmentId`, so both keys
|
|
1499
|
+
fell back to `'default'`, matched, and the feature appeared to work for a
|
|
1500
|
+
month.
|
|
1501
|
+
- **Proving test** — `web-sdk/customer-side-cooldown-suppression.test.ts`
|
|
1502
|
+
|
|
1503
|
+
### A dismissal is no longer treated as "no match"
|
|
1504
|
+
|
|
1505
|
+
- **What changed** — `FixedSurfaceSlot` rendered its `fallback` whenever
|
|
1506
|
+
`!visible`, and dismissing sets `visible: false` — so closing a placement put
|
|
1507
|
+
the fallback back in the same space, a ghost replacing the thing just
|
|
1508
|
+
dismissed. A slot now distinguishes the two: no match still renders `fallback`;
|
|
1509
|
+
a dismissal renders nothing. `FixedSurfaceSlot` also gains `onDismissed`, and
|
|
1510
|
+
`MessageSurfaceSlot`'s `onDismissed` — a public prop whose handler was built
|
|
1511
|
+
and then discarded, so it could never fire — now works.
|
|
1512
|
+
- **Who this breaks** — a host working around the ghost (one integration used a
|
|
1513
|
+
`MutationObserver`) can remove the workaround. `MessageSurfaceSlot`'s
|
|
1514
|
+
`onDismissed` goes from never firing to firing.
|
|
1515
|
+
- **Landed in** — `0.8.0`
|
|
1516
|
+
- **Fail-closed in** — same.
|
|
1517
|
+
- **Proving test** — `web-sdk/placements/FixedSurfaceSlot.dismissal.test.tsx`
|
|
1518
|
+
|
|
1519
|
+
### `convert()` retires the placement instead of only reporting it
|
|
1520
|
+
|
|
1521
|
+
- **What changed** — `convert()` emitted a `placement_interaction` event and
|
|
1522
|
+
stopped: it never wrote the terminal state, so **a user who converted kept
|
|
1523
|
+
being shown the upsell they had just paid for**. `dismiss()` and `snooze()` had
|
|
1524
|
+
the identical report-but-never-act shape. All three now route through
|
|
1525
|
+
`trackTreatmentInteraction`.
|
|
1526
|
+
A second defect sat behind the same symptom: nothing in the decision path ever
|
|
1527
|
+
*read* the terminal state. `isRetired` was written by `recordConversion` and
|
|
1528
|
+
had no caller, so a 5-minute transient window was the only thing hiding a
|
|
1529
|
+
converted placement — the upsell returned five minutes after checkout even
|
|
1530
|
+
through the controller path. `getPlacementDecision` now checks retirement and
|
|
1531
|
+
answers `retired_by_conversion`.
|
|
1532
|
+
- **Who this breaks** — `sdk.dismiss()` now writes suppression state where it
|
|
1533
|
+
previously did nothing.
|
|
1534
|
+
- **Landed in** — `0.8.0`
|
|
1535
|
+
- **Fail-closed in** — same.
|
|
1536
|
+
- **Proving test** — `web-sdk/convert-retires-placement.test.ts`
|
|
1537
|
+
|
|
1538
|
+
### New: a slot-inventory probe
|
|
1539
|
+
|
|
1540
|
+
- **What changed** — additive. `sdk.getRegisteredSlots()` returns the slots this
|
|
1541
|
+
app has mounted, and `sdk.diagnoseSlotInventory()` diffs them against the
|
|
1542
|
+
Playbook's placement triggers in both directions. A placement targeting a slot
|
|
1543
|
+
no code mounts **can never show** and nothing reported it; a config-side audit
|
|
1544
|
+
cannot see call sites.
|
|
1545
|
+
- **Landed in** — `0.8.0`
|
|
1546
|
+
- **Fail-closed in** — n/a, additive and diagnostic.
|
|
1547
|
+
- **Proving test** — `web-sdk/slot-inventory-probe.test.ts`
|
|
1548
|
+
|
|
1549
|
+
---
|
|
1550
|
+
|
|
1551
|
+
## 0.7.0
|
|
1552
|
+
|
|
1553
|
+
### `soft_block` → `block_with_upsell`
|
|
1554
|
+
|
|
1555
|
+
- **What changed** — the enforcement mode `soft_block` was renamed to
|
|
1556
|
+
`block_with_upsell`, and with it the reason codes the SDKs return:
|
|
1557
|
+
`usage_limit_reached_soft_block` → `usage_limit_reached_block_with_upsell`, and
|
|
1558
|
+
`credit_balance_exhausted_soft_block` → `credit_balance_exhausted_block_with_upsell`.
|
|
1559
|
+
**Any integration branching on the old reason string stops matching.**
|
|
1560
|
+
- **Landed in** — `0.7.0`
|
|
1561
|
+
- **Fail-closed in** — n/a. Behaviour is unchanged, and `soft_block` is still
|
|
1562
|
+
accepted wherever it appears in an already-compiled Playbook, decoding to
|
|
1563
|
+
exactly the decision it always made. Existing payloads keep working without
|
|
1564
|
+
recompilation; only the **string you match on** moved.
|
|
1565
|
+
- **Proving test** — the cross-language parity corpus (`tests/parity/`), which
|
|
1566
|
+
asserts the reason codes byte-identically across all three runtimes.
|
|
1567
|
+
|
|
1568
|
+
---
|
|
1569
|
+
|
|
1570
|
+
## 0.6.0
|
|
1571
|
+
|
|
1572
|
+
### The server-side decision methods are gone
|
|
1573
|
+
|
|
1574
|
+
- **What changed** — removed `RevTurbineServer.evaluate` / `getPlacement` /
|
|
1575
|
+
`checkEntitlement` / `can` / `getTrialStatus` (Node), and the whole
|
|
1576
|
+
`revturbine_server` module (Python).
|
|
1577
|
+
- **Why the breakage is nominal** — every removed method called a hosted decision
|
|
1578
|
+
endpoint that no longer exists, so each had been returning a **network error**
|
|
1579
|
+
since that endpoint was deleted, in two languages, unnoticed — because the
|
|
1580
|
+
cross-port gate only ever asked whether a method of that *name* existed.
|
|
1581
|
+
- **What stays** — `RevTurbineServer` itself, and `createClientSession()`, which
|
|
1582
|
+
mints the browser-safe token the client SDK's `clientSession` callback
|
|
1583
|
+
consumes. An integration using the class only for that is unaffected.
|
|
1584
|
+
- **Replacement** — evaluation is a pure function of (user context, Playbook) and
|
|
1585
|
+
runs in the SDK. Use `LocalEvaluationServer` (Node) or `RevTurbineCustomerSdk`
|
|
1586
|
+
(Python).
|
|
1587
|
+
- **Landed in** — `0.6.0`
|
|
1588
|
+
- **Fail-closed in** — effectively before `0.6.0`: the methods were already
|
|
1589
|
+
erroring. `0.6.0` made the failure a compile/import error instead of a runtime
|
|
1590
|
+
one.
|
|
1591
|
+
- **Proving test** — `tests/no-decision-endpoint.test.ts`
|
|
1592
|
+
|
|
1593
|
+
---
|
|
1594
|
+
|
|
1595
|
+
## 0.5.0
|
|
1596
|
+
|
|
1597
|
+
### The access gate denies during server rendering
|
|
1598
|
+
|
|
1599
|
+
- **What changed** — the gate is constructed inside a `useEffect`, and effects do
|
|
1600
|
+
not run during SSR. So on the server there was no gate at all: `isLoading` read
|
|
1601
|
+
false, `result` was null, `denied` computed false — and the gate emitted its
|
|
1602
|
+
**children**. The server-rendered HTML for a gated feature was literally the
|
|
1603
|
+
paid affordance. It now denies until a real verdict exists.
|
|
1604
|
+
- **Why this one mattered** — it is a fail-open in the one place the client
|
|
1605
|
+
cannot correct it. A crawler or a reader that never runs JS sees the paid
|
|
1606
|
+
content.
|
|
1607
|
+
- **Landed in** — `0.5.0`
|
|
1608
|
+
- **Fail-closed in** — `0.5.0`. This *is* the fail-closed change.
|
|
1609
|
+
- **Proving test** — `web-sdk/placements/AccessGateSurfaceSlot.ssr.test.tsx`
|
|
1610
|
+
|
|
1611
|
+
---
|
|
1612
|
+
|
|
1613
|
+
## 0.4.0
|
|
1614
|
+
|
|
1615
|
+
### An unresolvable plan identity denies in Python and Rust
|
|
1616
|
+
|
|
1617
|
+
- **What changed** — the headless ports granted where TypeScript denied when a
|
|
1618
|
+
user context carried no resolvable plan identity. The ports now deny, matching
|
|
1619
|
+
TypeScript.
|
|
1620
|
+
- **Landed in** — `0.4.0`
|
|
1621
|
+
- **Fail-closed in** — `0.4.0` for Python and Rust; TypeScript already denied.
|
|
1622
|
+
**The window between `0.3.0` and `0.4.0` is the expensive one** — a context
|
|
1623
|
+
that failed closed in TS silently granted in the other two runtimes.
|
|
1624
|
+
- **Proving test** — the cross-language parity corpus, which is what surfaced the
|
|
1625
|
+
divergence: the gate had been green while `deriveLocalEntitlementFromConfiguredRules`
|
|
1626
|
+
sat unreached by any fixture.
|
|
1627
|
+
|
|
1628
|
+
---
|
|
1629
|
+
|
|
1630
|
+
## 0.3.0
|
|
1631
|
+
|
|
1632
|
+
This release is where the `plan: { id }` → `plan_handle` migration landed. It was
|
|
1633
|
+
a **minor** bump carrying breaking changes, authorised because there were no
|
|
1634
|
+
active tenants and only in-org code referenced the changed surfaces.
|
|
1635
|
+
|
|
1636
|
+
### `plan: { id }` → `plan: { handle }` / `plan_handle`
|
|
1637
|
+
|
|
1638
|
+
- **What changed** — a plan's matching identity is its `unique_handle`.
|
|
1639
|
+
`plan.id` is DB-internal and no longer participates in matching anywhere, in
|
|
1640
|
+
any of the three runtimes.
|
|
1641
|
+
- **Symptom if you do not migrate** — a context whose only plan signal is
|
|
1642
|
+
`plan.id` now matches **no** plan-targeted rule and fails closed. It does not
|
|
1643
|
+
error; it simply has no plan.
|
|
1644
|
+
- **Landed in** — `0.3.0`
|
|
1645
|
+
- **Fail-closed in** — `0.3.0`.
|
|
1646
|
+
- **Proving test** — `web-sdk/user-context-exactness.test-d.ts` plus the
|
|
1647
|
+
plan-identity parity fixtures
|
|
1648
|
+
(`tests/parity/fixtures/entitlement_plan_identity_is_handle.json`,
|
|
1649
|
+
`entitlement_no_plan_identity_denies.json`).
|
|
1650
|
+
|
|
1651
|
+
### `identify()` no longer accepts an arbitrary traits bag
|
|
1652
|
+
|
|
1653
|
+
- **What changed** — unknown top-level context keys are a compile error.
|
|
1654
|
+
Free-form values go under `custom`, via `update()`.
|
|
1655
|
+
- **Landed in** — `0.3.0`
|
|
1656
|
+
- **Fail-closed in** — `0.3.0`, at compile time.
|
|
1657
|
+
- **Proving test** — `web-sdk/user-context-exactness.test-d.ts`
|
|
1658
|
+
|
|
1659
|
+
### `local_runtime_default_allow` → `entitlement_not_in_playbook`
|
|
1660
|
+
|
|
1661
|
+
- **What changed** — the reason code was renamed with **no deprecated alias**,
|
|
1662
|
+
and the headless runtimes' terminal fallback now **denies** where it used to
|
|
1663
|
+
grant.
|
|
1664
|
+
- **Landed in** — `0.3.0`
|
|
1665
|
+
- **Fail-closed in** — `0.3.0`. The rename and the fail-closed flip shipped
|
|
1666
|
+
together, deliberately: the old name described a grant, so keeping it as an
|
|
1667
|
+
alias would have described the new behaviour incorrectly.
|
|
1668
|
+
- **Proving test** — the fail-closed suites in `web-sdk/` and the corresponding
|
|
1669
|
+
Python and Rust port suites.
|
|
1670
|
+
|
|
1671
|
+
---
|
|
1672
|
+
|
|
1673
|
+
## Before 0.3.0
|
|
1674
|
+
|
|
1675
|
+
Versions `0.1.0` through `0.2.91` predate this changelog. Breaking changes in
|
|
1676
|
+
that range are recorded only in commit history and release notes. If you are
|
|
1677
|
+
upgrading from `0.2.x`, read the `0.3.0` section first — it is the release that
|
|
1678
|
+
changed plan identity, and it is the one most likely to affect you.
|