@chipmobilesdk/rn-notification 0.3.0 → 0.4.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.
Files changed (75) hide show
  1. package/CHANGELOG.md +174 -109
  2. package/README.md +414 -399
  3. package/package.json +4 -21
  4. package/skills/sdk-rn-notification/SKILL.md +5 -4
  5. package/src/channel/migrate.ts +285 -285
  6. package/src/channel/tone-map.ts +130 -130
  7. package/src/channel/validate-tone.ts +61 -61
  8. package/src/contract/channel.ts +192 -188
  9. package/src/contract/diagnostics.ts +102 -102
  10. package/src/contract/errors.ts +134 -130
  11. package/src/contract/index.ts +10 -10
  12. package/src/contract/interaction.ts +62 -62
  13. package/src/contract/notification.ts +118 -118
  14. package/src/contract/permission.ts +55 -60
  15. package/src/contract/reconcile.ts +88 -88
  16. package/src/contract/recurrence.ts +129 -123
  17. package/src/diagnostics/burst.ts +101 -101
  18. package/src/diagnostics/events.ts +72 -72
  19. package/src/diagnostics/redact.ts +72 -72
  20. package/src/diagnostics/snapshot.ts +67 -64
  21. package/src/engine/adapter.ts +172 -172
  22. package/src/engine/codec.ts +89 -0
  23. package/src/engine/notify-kit-engine.ts +706 -761
  24. package/src/index.ts +208 -208
  25. package/src/initialize.ts +240 -174
  26. package/src/interaction/index.ts +146 -146
  27. package/src/maintenance/cycle.ts +150 -147
  28. package/src/maintenance/permission-refresh.ts +39 -41
  29. package/src/maintenance/reanchor.ts +101 -95
  30. package/src/maintenance/toggle.ts +66 -60
  31. package/src/maintenance/verify.ts +83 -83
  32. package/src/maintenance/window-refresh.ts +64 -61
  33. package/src/permission/index.ts +183 -183
  34. package/src/permission/state.ts +77 -77
  35. package/src/push/envelope.ts +165 -165
  36. package/src/push/fcm-provider.ts +143 -146
  37. package/src/push/gate.ts +190 -190
  38. package/src/push/index.ts +33 -29
  39. package/src/push/instructions.ts +114 -109
  40. package/src/push/provider-adapter.ts +54 -54
  41. package/src/push/signout.ts +65 -65
  42. package/src/reconcile/chunk.ts +53 -53
  43. package/src/reconcile/diff.ts +142 -145
  44. package/src/reconcile/duplicates.ts +73 -73
  45. package/src/reconcile/index.ts +180 -188
  46. package/src/reconcile/truncate.ts +56 -56
  47. package/src/recurrence/clock-guard.ts +73 -71
  48. package/src/recurrence/expand.ts +326 -292
  49. package/src/recurrence/index.ts +270 -177
  50. package/src/repeat/expiry.ts +53 -55
  51. package/src/repeat/index.ts +54 -54
  52. package/src/repeat/marker.ts +72 -72
  53. package/src/repeat/place.ts +73 -73
  54. package/src/schedule/anchor.ts +278 -267
  55. package/src/schedule/index.ts +174 -184
  56. package/src/schedule/partition.ts +88 -85
  57. package/src/schedule/resolve-moment.ts +80 -80
  58. package/src/schedule/validate-content.ts +111 -111
  59. package/src/schedule/validate-identifier.ts +126 -126
  60. package/src/schedule/writer.ts +55 -0
  61. package/src/state/caps.ts +71 -71
  62. package/src/state/dedup-ledger.ts +35 -35
  63. package/src/state/interaction-buffer.ts +73 -73
  64. package/src/state/mmkv-store.ts +75 -75
  65. package/src/state/permission-history.ts +41 -41
  66. package/src/state/record.ts +266 -258
  67. package/src/state/reset.ts +41 -41
  68. package/src/state/series-ledger.ts +91 -84
  69. package/src/state/store.ts +40 -40
  70. package/src/testing/conformance/index.ts +258 -248
  71. package/src/testing/fake-engine.ts +462 -469
  72. package/src/testing/fake-provider.ts +126 -129
  73. package/src/testing/faults.ts +72 -72
  74. package/src/testing/index.ts +23 -28
  75. package/src/testing/measure.ts +337 -337
package/CHANGELOG.md CHANGED
@@ -1,109 +1,174 @@
1
- # Changelog
2
-
3
- All notable changes to `@chipmobilesdk/rn-notification` are documented here.
4
-
5
- The format follows [Keep a Changelog](https://keepachangelog.com/en/1.1.0/), and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
6
-
7
- ## [0.3.0] — 2026-10-13
8
-
9
- Documentation release: the agent skill now matches the code `0.2.1` shipped. **No code,
10
- API, storage or behaviour change** — upgrading from `0.2.1` changes nothing at runtime.
11
- Released as its own version so the skill a consuming app syncs (`packageVersion`) always
12
- matches the package it came from.
13
-
14
- ### Changed
15
-
16
- - Skill, `## Debugging "it did not arrive"`: check the installed version first — before
17
- `0.2.1` the state record never persisted on a device, so deduplication and cold-start
18
- tap buffering lasted one session. Require `0.2.1` or later.
19
-
20
- ## [0.2.1] — 2026-10-12
21
-
22
- Patch release. No consumer change required.
23
-
24
- ### Fixed
25
-
26
- - **The state store never reached the device's storage.** It opened MMKV with
27
- `new MMKV()`, the react-native-mmkv **v3** constructor. In v4 — the only major the peer
28
- range (`>=4.0.0`) allows — `MMKV` is a type, not a value, so the call threw and the
29
- package fell back to its memory store on every device. Deduplication and cold-start
30
- buffering were therefore session-scoped — the documented degradation for a missing
31
- native module, applied where the module was present. The store now uses v4's
32
- `createMMKV` and `remove`.
33
- - The repository's test double exported a v3 `MMKV` class, so no test could see it. The
34
- double now mirrors v4, and `mmkv-store.test.ts` fails against the old code.
35
-
36
- ## [0.2.0]
37
-
38
- Additive minor, delivering on the reserved **additional tone attributes** commitment below. **No consumer source change, no configuration change, and no tone version bump is required.**
39
-
40
- ### Added
41
-
42
- - **Alarm-capable tones.** `ToneDeclaration` gains `repeatAlert` — an alert sound that repeats until the user handles the notification — and `timeSensitive`, which asks the platform for its time-sensitive treatment. `repeatAlert` carries a **required** duration, so an unbounded repeating alert is unrepresentable rather than merely discouraged; the range is 1 to 60 minutes, exported as `REPEAT_ALERT_BOUNDS`.
43
- - **A repeating alert stops on its own, and the notification stays.** When the declared window elapses the sound ends and the notification remains in the tray. Handling it earlier stops the sound immediately. Both hold with the app process dead, which is where the expiry almost always falls.
44
- - **`registerBackgroundNotificationHandler`.** The one piece a consumer wires up themselves, at module scope in `index.js`. Required only by an app that declares a repeating tone; its absence is reported as `REPEAT_STOP_HANDLER_MISSING` rather than discovered as a phone that will not stop ringing.
45
- - **`ChannelApplyResult.labelsRefreshed`** and **`ChannelApplyResult.repeatUnavailable`**, so a caller can tell a label correction from a creation, a migration, and a no-op — and can see which attributes the platform refused.
46
- - Four diagnostic identifiers: `INVALID_TONE_REPEAT`, and the non-failures `TONE_LABELS_REFRESHED`, `REPEAT_ALERT_UNAVAILABLE`, `REPEAT_STOP_HANDLER_MISSING`. The stable set is now **43**.
47
-
48
- ### Fixed
49
-
50
- - **Tone labels reach system settings.** `ToneDeclaration.name` was documented as the name shown in system settings and never reached one: `ResolvedTone` carried no label at all, so the adapter passed the tone *identifier* as the channel name. Users read `reminder` where a consumer had written a localized label. `description` shared the same fate.
51
- - **A tone label can be corrected after the first install.** Declaring a tone the device already held returned early, so a label could be set exactly once per device, forever. Android permits updating a channel's name and description in place — and ignores everything else, which is precisely what keeps the user's own importance, sound, and Do-Not-Disturb choices intact. Correcting a label now costs no version bump and no user setting.
52
-
53
- ### Changed
54
-
55
- - **One-time label refresh on upgrade.** The first `apply()` after upgrading performs a single label-refresh write per existing tone, because every channel on every existing device currently carries the tone id as its name. Every apply after that is zero-operation again, as before. Nothing is recreated, retired, re-homed, or cancelled.
56
- - **`NotificationEngineAdapter` gained members** (*Experimental* per the stability table below): `declareTone` now returns `'created' | 'labelsRefreshed' | 'unchanged'` instead of a boolean, `EngineCapabilities` gained `supportsRepeatingAlert`, and the port gained `silenceDelivered`, `listRepeatStops`, `subscribeToDeliveries`, `registerBackgroundDeliveryHandler`, and `hasBackgroundDeliveryHandler`. Only an app supplying its own engine is affected.
57
- - **`cms.stop:` is now a reserved identifier prefix.** Repeat expiry cancels its markers by identifier, so a consumer identifier that looked like one would be deleted out from under a real notification.
58
-
59
- ### Platform limits — stated rather than papered over
60
-
61
- - **iOS does not repeat an alert sound.** There is no looping API, and the one interruption level that overrides the mute switch requires an entitlement Apple grants by application — which this package does not hold and does not request. A repeating alert on iOS is reported in `repeatUnavailable`; the notification still delivers and still sounds once.
62
- - **iOS `timeSensitive` needs the consuming app's own Time Sensitive Notifications capability.** Without it iOS delivers at `active`. This was already true for `urgency: 'max'` in 0.1.x and was previously undocumented.
63
- - **The declared window is a floor, not a precise stop.** The entry that ends the alert is scheduled like any other notification and inherits the same delivery accuracy — within 60 s with exact alarms, up to 15 minutes without. Measured on an emulator without `SCHEDULE_EXACT_ALARM`: a declared 2-minute window ran 2m35s and 3m00s.
64
- - **Android alarm audio uses the notification volume, not the alarm volume.** A device with notifications muted stays silent. Not fixable without native changes.
65
- - **Android also stops an insistent sound when the user opens the notification shade** — platform behaviour, and arguably the right one.
66
- - **The stop marker is briefly visible in the tray** before it is removed, on a minimum-importance, silent channel. A scheduled notification is the only OS-driven execution the engine offers.
67
- - **Without the registered background handler, a repeating alert does not stop while the app is dead.**
68
- - **Aggressive vendor battery management can delay marker delivery**, in the same way and for the same reasons the package already reports `mayBeDelayed` and `unknown`.
69
-
70
- ### Migration
71
-
72
- None required. Every addition is optional, `repeatAlert` and `timeSensitive` are realized per notification rather than as channel configuration, and labels are updated in place — so no tone version moves and no channel is recreated. Only an app that declares a repeating tone needs the new setup step in `index.js`.
73
-
74
- ## [Unreleased] — 0.1.0
75
-
76
- First release. New package, no existing consumers, no migration burden.
77
-
78
- ### Added
79
-
80
- - **Scheduling** — caller-supplied identifiers, replacement semantics, a two-sided 60-second tolerance window, and full-detail pending reads that let a consumer diff the device against its own intent.
81
- - **Time anchoring** — wall-clock (default) and absolute modes, with re-anchoring on device time-zone change and an `anchoringGeneration` on every entry so the one case re-anchoring cannot fix stays detectable.
82
- - **Reconciliation** — desired-set convergence that is idempotent, domain-scoped, chunked, and serialized against itself.
83
- - **Recurrence** — declare a rule once; the package maintains a bounded 60-day / 32-occurrence window and refreshes it, with deterministic daylight-saving resolution and clock-discontinuity bounding.
84
- - **Permissions** — six states that never collapse, independent display and exact-alarm requests, per-permission settings deep links, external-change detection, and manufacturer restriction reporting that admits `unknown`.
85
- - **Channels and tones** — one declarative configuration mapped to each platform, with a real migration path for Android's immutable channels.
86
- - **Interaction** — one handler for foreground, background, and cold start, backed by a durable buffer that survives process death.
87
- - **Push** — a provider-agnostic gate with a normalized envelope, token lifecycle, deduplication across process death, and best-effort handling of newer payload versions.
88
- - **Observability** — a structured event stream with content redaction, a state snapshot, burst detection, fault injection, and a shared conformance suite.
89
-
90
- ### Notes
91
-
92
- - **Local engine substitution — approved 2026-08-16.** The specification named Notifee as the default engine. Its repository was archived on 2026-04-07 with its last release in December 2024, failing the constitution's actively-maintained-dependency requirement. This package uses `react-native-notify-kit`, the maintained 100%-API-compatible fork, **confirmed by the project owner**. The engine sits behind `NotificationEngineAdapter` and no consumer-facing behaviour depends on the choice — this substitution changed no functional requirement, success criterion, or user story. `expo-notifications` is the recorded migration target should the fork stall.
93
- - **Push is an optional peer.** An app that never imports `/push` ships no provider code, no push permissions, and no entitlements, and takes on none of the provider's data-collection declarations.
94
- - **No exact-alarm permission is declared by this package.** Consumers opt in; without it, notifications are scheduled in approximate mode with an explicit `mayBeDelayed` flag. `USE_EXACT_ALARM` is never recommended — Google Play restricts it to alarm, timer, and calendar apps.
95
-
96
- ### Stability
97
-
98
- | Surface | Stability |
99
- |---|---|
100
- | Identifier rules | Stable — breaking to change (persisted by the platform on user devices) |
101
- | Routing payload contract | Stable — breaking to change |
102
- | Push payload contract | Stable — **append-only**; repurposing a released field is a contract violation, not a version bump |
103
- | Scheduling, reconciliation, permissions, interaction | Stable — additive minor releases only |
104
- | Adapter ports | Experimental — may gain members in minor releases |
105
- | Persisted state schema | Private — incompatible versions are discarded and rebuilt, never migrated |
106
-
107
- ### Reserved for additive minor releases
108
-
109
- Additional push provider adapters, additional recurrence kinds, ~~additional tone attributes~~ *(delivered in 0.2.0)*, additional diagnostic identifiers, a consumer-supplied storage port, and in-app notification presentation.
1
+ # @chipmobilesdk/rn-notification
2
+
3
+ ## 0.4.1
4
+
5
+ ### Patch Changes
6
+
7
+ - Packaging only; no code change. The package no longer declares `engines.node`: a
8
+ React Native package does not run on Node, so the field only produced install
9
+ warnings in apps on another Node version. rn-i18n's command-line tooling does run
10
+ on Node; its README's compatibility table states the version it needs. ([#14](https://github.com/operationd8979/chip-mobile-sdk/pull/14))
11
+
12
+ ## [0.4.0] — 2026-11-01
13
+
14
+ Fixes two defects that delivered the wrong number of notifications, plus the
15
+ maintenance and write-path gaps behind them. **One breaking change**: occurrence
16
+ identifiers changed format (see Changed). Existing series migrate on their next
17
+ refresh with no app action.
18
+
19
+ ### Fixed
20
+
21
+ - **A count-limited series delivers exactly its count.** Every window refresh added
22
+ the still-pending occurrences to the emitted count again, so a series with
23
+ `count` larger than `occurrencesPerSeries` ended after roughly half its
24
+ occurrences. The ledger now records the last emitted occurrence and counts each
25
+ occurrence once.
26
+ - **A time-zone change no longer duplicates occurrences.** Re-anchoring kept an
27
+ occurrence's identifier while the next window refresh derived a new one from the
28
+ new instant, so the same slot was scheduled twice. Identifiers now come from the
29
+ calendar slot and do not change with the zone.
30
+ - **Every write keeps its stop marker in step.** Recurrence, re-anchoring and channel
31
+ re-homing wrote straight to the engine, so a repeating alarm in a series never got
32
+ a stop marker and a re-anchored one stopped at the old instant. All write paths now
33
+ go through one internal `ScheduleWriter`. Recurrence also respects the platform
34
+ ceiling, and `registerSeries` validates its template as a direct schedule would —
35
+ an undeclared tone throws `NotificationError(UNDECLARED_TONE)`, not a plain `Error`.
36
+ - **Reconcile is idempotent on a device for notifications with actions.** The real
37
+ engine never read `actions` back, so every pass replaced them; the fake stored
38
+ requests verbatim and hid it. Actions now round-trip through the data bag, and the
39
+ fake goes through the same codec as the real engine.
40
+ - **Automatic maintenance runs.** `initialize()` now applies `config.tones` (and any
41
+ channel migration) and runs the steps registered on `runtime.registry` when the app
42
+ returns to the foreground, triggered as `timeZoneChange` when the zone moved.
43
+ Ownership is decided per pending occurrence, so a legitimate cancel is no longer
44
+ reported as a `MaintenanceScopeViolation`. `durationMs` is measured.
45
+ - **A maintenance call that overlaps a running cycle waits for it.** It used to
46
+ return an empty result at once. With the automatic foreground cycle above, an app
47
+ that runs maintenance and then reconciles on the same foreground event got that
48
+ empty result and reconciled while the window refresh was still writing, so a
49
+ reminder the reconcile cancelled could be scheduled again. Overlapping calls now
50
+ join the running cycle and receive its result; the work still runs once.
51
+
52
+ ### Changed
53
+
54
+ - **Breaking:** occurrence identifiers are `<seriesId>#YYYYMMDDTHHmm` instead of
55
+ `<seriesId>#<epoch ms>`, and `occurrenceId(seriesId, slot)` takes wall-clock parts
56
+ or a slot key instead of an instant. Occurrences already on a device under the old
57
+ format are cancelled and rescheduled on the series' next refresh. A count-limited
58
+ series registered under 0.3 keeps the count it had recorded, which may be too high.
59
+ - `InitializeResult.tonesApplied` and `migrationsRun` report real values instead of `0`.
60
+
61
+ ### Added
62
+
63
+ - `NotificationRuntime.channels`, `lastAutomaticMaintenance()` and `dispose()`.
64
+ - `RuntimeDependencies.appState` and `timeZone`, for tests.
65
+ - `SeriesResult.cancelled`; the window-refresh step reports those identifiers to the
66
+ scope guard.
67
+ - Diagnostic code `MAINTENANCE_FAILED`.
68
+
69
+ ## [0.3.0] — 2026-10-13
70
+
71
+ Documentation release: the agent skill now matches the code `0.2.1` shipped. **No code,
72
+ API, storage or behaviour change** — upgrading from `0.2.1` changes nothing at runtime.
73
+ Released as its own version so the skill a consuming app syncs (`packageVersion`) always
74
+ matches the package it came from.
75
+
76
+ ### Changed
77
+
78
+ - Skill, `## Debugging "it did not arrive"`: check the installed version first — before
79
+ `0.2.1` the state record never persisted on a device, so deduplication and cold-start
80
+ tap buffering lasted one session. Require `0.2.1` or later.
81
+
82
+ ## [0.2.1] — 2026-10-12
83
+
84
+ Patch release. No consumer change required.
85
+
86
+ ### Fixed
87
+
88
+ - **The state store never reached the device's storage.** It opened MMKV with
89
+ `new MMKV()`, the react-native-mmkv **v3** constructor. In v4 — the only major the peer
90
+ range (`>=4.0.0`) allows — `MMKV` is a type, not a value, so the call threw and the
91
+ package fell back to its memory store on every device. Deduplication and cold-start
92
+ buffering were therefore session-scoped — the documented degradation for a missing
93
+ native module, applied where the module was present. The store now uses v4's
94
+ `createMMKV` and `remove`.
95
+ - The repository's test double exported a v3 `MMKV` class, so no test could see it. The
96
+ double now mirrors v4, and `mmkv-store.test.ts` fails against the old code.
97
+
98
+ ## [0.2.0] — 2026-08-30
99
+
100
+ Additive minor, delivering on the reserved **additional tone attributes** commitment below. **No consumer source change, no configuration change, and no tone version bump is required.**
101
+
102
+ ### Added
103
+
104
+ - **Alarm-capable tones.** `ToneDeclaration` gains `repeatAlert` — an alert sound that repeats until the user handles the notification — and `timeSensitive`, which asks the platform for its time-sensitive treatment. `repeatAlert` carries a **required** duration, so an unbounded repeating alert is unrepresentable rather than merely discouraged; the range is 1 to 60 minutes, exported as `REPEAT_ALERT_BOUNDS`.
105
+ - **A repeating alert stops on its own, and the notification stays.** When the declared window elapses the sound ends and the notification remains in the tray. Handling it earlier stops the sound immediately. Both hold with the app process dead, which is where the expiry almost always falls.
106
+ - **`registerBackgroundNotificationHandler`.** The one piece a consumer wires up themselves, at module scope in `index.js`. Required only by an app that declares a repeating tone; its absence is reported as `REPEAT_STOP_HANDLER_MISSING` rather than discovered as a phone that will not stop ringing.
107
+ - **`ChannelApplyResult.labelsRefreshed`** and **`ChannelApplyResult.repeatUnavailable`**, so a caller can tell a label correction from a creation, a migration, and a no-op — and can see which attributes the platform refused.
108
+ - Four diagnostic identifiers: `INVALID_TONE_REPEAT`, and the non-failures `TONE_LABELS_REFRESHED`, `REPEAT_ALERT_UNAVAILABLE`, `REPEAT_STOP_HANDLER_MISSING`. The stable set is now **43**.
109
+
110
+ ### Fixed
111
+
112
+ - **Tone labels reach system settings.** `ToneDeclaration.name` was documented as the name shown in system settings and never reached one: `ResolvedTone` carried no label at all, so the adapter passed the tone *identifier* as the channel name. Users read `reminder` where a consumer had written a localized label. `description` shared the same fate.
113
+ - **A tone label can be corrected after the first install.** Declaring a tone the device already held returned early, so a label could be set exactly once per device, forever. Android permits updating a channel's name and description in place — and ignores everything else, which is precisely what keeps the user's own importance, sound, and Do-Not-Disturb choices intact. Correcting a label now costs no version bump and no user setting.
114
+
115
+ ### Changed
116
+
117
+ - **One-time label refresh on upgrade.** The first `apply()` after upgrading performs a single label-refresh write per existing tone, because every channel on every existing device currently carries the tone id as its name. Every apply after that is zero-operation again, as before. Nothing is recreated, retired, re-homed, or cancelled.
118
+ - **`NotificationEngineAdapter` gained members** (*Experimental* per the stability table below): `declareTone` now returns `'created' | 'labelsRefreshed' | 'unchanged'` instead of a boolean, `EngineCapabilities` gained `supportsRepeatingAlert`, and the port gained `silenceDelivered`, `listRepeatStops`, `subscribeToDeliveries`, `registerBackgroundDeliveryHandler`, and `hasBackgroundDeliveryHandler`. Only an app supplying its own engine is affected.
119
+ - **`cms.stop:` is now a reserved identifier prefix.** Repeat expiry cancels its markers by identifier, so a consumer identifier that looked like one would be deleted out from under a real notification.
120
+
121
+ ### Platform limits — stated rather than papered over
122
+
123
+ - **iOS does not repeat an alert sound.** There is no looping API, and the one interruption level that overrides the mute switch requires an entitlement Apple grants by application — which this package does not hold and does not request. A repeating alert on iOS is reported in `repeatUnavailable`; the notification still delivers and still sounds once.
124
+ - **iOS `timeSensitive` needs the consuming app's own Time Sensitive Notifications capability.** Without it iOS delivers at `active`. This was already true for `urgency: 'max'` in 0.1.x and was previously undocumented.
125
+ - **The declared window is a floor, not a precise stop.** The entry that ends the alert is scheduled like any other notification and inherits the same delivery accuracy — within 60 s with exact alarms, up to 15 minutes without. Measured on an emulator without `SCHEDULE_EXACT_ALARM`: a declared 2-minute window ran 2m35s and 3m00s.
126
+ - **Android alarm audio uses the notification volume, not the alarm volume.** A device with notifications muted stays silent. Not fixable without native changes.
127
+ - **Android also stops an insistent sound when the user opens the notification shade** — platform behaviour, and arguably the right one.
128
+ - **The stop marker is briefly visible in the tray** before it is removed, on a minimum-importance, silent channel. A scheduled notification is the only OS-driven execution the engine offers.
129
+ - **Without the registered background handler, a repeating alert does not stop while the app is dead.**
130
+ - **Aggressive vendor battery management can delay marker delivery**, in the same way and for the same reasons the package already reports `mayBeDelayed` and `unknown`.
131
+
132
+ ### Migration
133
+
134
+ None required. Every addition is optional, `repeatAlert` and `timeSensitive` are realized per notification rather than as channel configuration, and labels are updated in place — so no tone version moves and no channel is recreated. Only an app that declares a repeating tone needs the new setup step in `index.js`.
135
+
136
+ ## [0.1.2] — 2026-08-29
137
+
138
+ The first release. 0.1.0 and 0.1.1 (2026-08-17) were published from the feature
139
+ branch before it merged; 0.1.2 is the merged release of the same work.
140
+
141
+ First release. New package, no existing consumers, no migration burden.
142
+
143
+ ### Added
144
+
145
+ - **Scheduling** — caller-supplied identifiers, replacement semantics, a two-sided 60-second tolerance window, and full-detail pending reads that let a consumer diff the device against its own intent.
146
+ - **Time anchoring** — wall-clock (default) and absolute modes, with re-anchoring on device time-zone change and an `anchoringGeneration` on every entry so the one case re-anchoring cannot fix stays detectable.
147
+ - **Reconciliation** — desired-set convergence that is idempotent, domain-scoped, chunked, and serialized against itself.
148
+ - **Recurrence** — declare a rule once; the package maintains a bounded 60-day / 32-occurrence window and refreshes it, with deterministic daylight-saving resolution and clock-discontinuity bounding.
149
+ - **Permissions** — six states that never collapse, independent display and exact-alarm requests, per-permission settings deep links, external-change detection, and manufacturer restriction reporting that admits `unknown`.
150
+ - **Channels and tones** — one declarative configuration mapped to each platform, with a real migration path for Android's immutable channels.
151
+ - **Interaction** — one handler for foreground, background, and cold start, backed by a durable buffer that survives process death.
152
+ - **Push** — a provider-agnostic gate with a normalized envelope, token lifecycle, deduplication across process death, and best-effort handling of newer payload versions.
153
+ - **Observability** — a structured event stream with content redaction, a state snapshot, burst detection, fault injection, and a shared conformance suite.
154
+
155
+ ### Notes
156
+
157
+ - **Local engine substitution — approved 2026-08-16.** The specification named Notifee as the default engine. Its repository was archived on 2026-04-07 with its last release in December 2024, failing the constitution's actively-maintained-dependency requirement. This package uses `react-native-notify-kit`, the maintained 100%-API-compatible fork, **confirmed by the project owner**. The engine sits behind `NotificationEngineAdapter` and no consumer-facing behaviour depends on the choice — this substitution changed no functional requirement, success criterion, or user story. `expo-notifications` is the recorded migration target should the fork stall.
158
+ - **Push is an optional peer.** An app that never imports `/push` ships no provider code, no push permissions, and no entitlements, and takes on none of the provider's data-collection declarations.
159
+ - **No exact-alarm permission is declared by this package.** Consumers opt in; without it, notifications are scheduled in approximate mode with an explicit `mayBeDelayed` flag. `USE_EXACT_ALARM` is never recommended — Google Play restricts it to alarm, timer, and calendar apps.
160
+
161
+ ### Stability
162
+
163
+ | Surface | Stability |
164
+ |---|---|
165
+ | Identifier rules | Stable — breaking to change (persisted by the platform on user devices) |
166
+ | Routing payload contract | Stable — breaking to change |
167
+ | Push payload contract | Stable — **append-only**; repurposing a released field is a contract violation, not a version bump |
168
+ | Scheduling, reconciliation, permissions, interaction | Stable — additive minor releases only |
169
+ | Adapter ports | Experimental — may gain members in minor releases |
170
+ | Persisted state schema | Private — incompatible versions are discarded and rebuilt, never migrated |
171
+
172
+ ### Reserved for additive minor releases
173
+
174
+ Additional push provider adapters, additional recurrence kinds, ~~additional tone attributes~~ *(delivered in 0.2.0)*, additional diagnostic identifiers, a consumer-supplied storage port, and in-app notification presentation.