@wireai/activation 0.14.0 → 0.14.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/CHANGELOG.md CHANGED
@@ -3,6 +3,61 @@
3
3
  All notable changes to `@wireai/activation` (formerly `wireai-onboarding`).
4
4
  Historical entries below the rename keep the old package name on purpose.
5
5
 
6
+ ## [0.14.1] - 2026-08-18
7
+
8
+ Two rulings from the 0.14.0 audit, applied. Both were reproduced there and left for a decision
9
+ rather than guessed at; both decisions came back on 2026-08-18.
10
+
11
+ **PATCH: no public surface gained, lost or renamed a member.** But read the first entry before you
12
+ upgrade — it is a behaviour change you will see in your funnel, not a silent internal fix.
13
+
14
+ ### Fixed
15
+
16
+ - **The review/questionnaire gates counted JS PROCESSES, not app-opens, so `minSessions` stalled at
17
+ 1 on iOS.** `useLifecycleEvents` already defines an app-open correctly — a foreground after 30+
18
+ minutes in the background — and fires a fresh `app.session_started` for it. The device-side
19
+ counter behind `minSessions` did not follow: it pinned one id for the life of the JS process, and
20
+ iOS suspends apps rather than killing them. So a user who opened the app every day for a week
21
+ produced seven server-side opens while `wire_review_<id>_sessions` stayed at 1, and any gate with
22
+ the fail-closed `minSessions: 2` default could never fire on a phone that is never force-quit —
23
+ which is most phones. **⚠️ UPGRADE NOTE: gates that have been silently dormant will start firing.**
24
+ That is the fix, but if you have been reading a flat prompt rate as "users decline", the number
25
+ will move. The counter still refuses the case it was pinned for in the first place: on a cold
26
+ start the gate renders (and counts) before the root lifecycle effect registers the session id, and
27
+ that first registration NAMES the open already counted rather than starting a second one — so the
28
+ 2026-07-16 incident, where `minSessions: 2` became satisfiable inside a user's very first launch,
29
+ stays closed. One accepted under-count remains, deliberately: if the mount open never registers (a
30
+ refused non-durable device key over a broken store) and a later foreground is the first
31
+ registration the launch sees, it is adopted rather than counted. A fail-closed gate may only err
32
+ toward prompting less.
33
+
34
+ - **A permission record that could not be READ is no longer read as "nothing settled".** A timed-out
35
+ or throwing storage read arrived as `[]`, which is indistinguishable from a genuine empty record,
36
+ and the flow acted on that guess twice: it re-showed a permission screen the user may already have
37
+ answered — spending the ONE native prompt iOS ever grants — and the answer then persisted a record
38
+ containing just that one id, OVER whatever the store actually held. A slow adapter could therefore
39
+ permanently shrink the memory that exists to prevent the re-ask. The read now reports whether the
40
+ store answered at all; on "it did not", permission screens are suppressed for that session and
41
+ nothing is written back. A successful read of a corrupt, absent or other-session entry is still a
42
+ genuine empty and still shows the screen. The trade is stated rather than hidden: on a host whose
43
+ adapter never answers, configured permission screens silently never show, so it warns in dev
44
+ naming the adapter.
45
+
46
+ ### Changed
47
+
48
+ - **`@wireai/activation:processOpenId` is re-declared LIVE (was LATCH).** The governance canary in
49
+ `test/canary/globalSlotDiscipline.test.tsx` asserted that this slot must NOT follow the session
50
+ registry, which is what froze the counter above; it now drives the slot through two real opens and
51
+ requires it to move. The cold-start property it used to guard — the first registration of a launch
52
+ must not re-pin — is asserted in `src/reviews/sessionCount.test.ts`, alongside the incident it
53
+ exists for. The slot holds the pinned open id plus the registered id observed when it was pinned,
54
+ because "is this a new open?" is only answerable against what was last seen.
55
+
56
+ Tests 808 → 814, all green, and the deferred `todo` reproduction is now a passing assertion.
57
+ Every fix was falsified first: reverting the counter fix reddens exactly the app-open test plus the
58
+ slot canary and leaves the cold-start test green; collapsing the unknown read back to empty reddens
59
+ both unit tests and the end-to-end canary while its positive control stays green.
60
+
6
61
  ## [0.14.0] - 2026-08-17
7
62
 
8
63
  A durability release, in two rounds. Round 1 stopped the two counted lifecycle events from riding a
package/README.md CHANGED
@@ -1162,6 +1162,11 @@ const gate = useReviewGate({ config: { id: "home", minSessions: 2 }, decision, s
1162
1162
  await track("journal_done"); // POST stored → revalidation bumps → effect re-fetches → gate can fire
1163
1163
  ```
1164
1164
 
1165
+ `minSessions` counts **app-opens**, not mounts and not launches: a foreground after 30+ minutes in
1166
+ the background is a new one. That matters on iOS, where an app is suspended rather than killed — a
1167
+ user who never force-quits still advances the counter. (Before 0.14.1 it counted JS processes, so
1168
+ on those devices it stayed at 1 forever and a `minSessions: 2` gate could never fire.)
1169
+
1165
1170
  The questionnaire gate uses the identical shape: swap `fetchReviewDecision` / `useReviewGate`
1166
1171
  for `fetchQuestionnaireDecision` / `useQuestionnaireGate` from `@wireai/activation/questionnaire`
1167
1172
  (both take a `decision` prop). One `wire.track` call feeds every subscribed gate.