proactive-gate 0.2.0 → 0.2.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/README.md CHANGED
@@ -112,6 +112,22 @@ consumed in check order at commit, so when a weekly check passes and the daily o
112
112
  refuses, that weekly unit is spent without a delivery. It only happens when two commits
113
113
  race after a shared evaluate.
114
114
 
115
+ ### Two limits you should know before you adopt this
116
+
117
+ Neither is a bug, and both are pinned by tests so a future change has to be deliberate.
118
+
119
+ **The week is the ISO week, so the weekly budget refills on Monday.** Where the working
120
+ week runs Sunday to Thursday, that refill lands one day in: a user who spends the budget on
121
+ Sunday has it back on Monday, with four working days still to run. Changing the key would
122
+ move every counter already in your store, so it is documented rather than quietly altered.
123
+ Pass your own budget check keyed how you like if the ISO week is wrong for your users.
124
+
125
+ **Quiet hours are a single window, the same on every day of the week.** A user carries one
126
+ `start` and one `end`, so a Friday window, a Shabbat window or a public holiday cannot be
127
+ expressed. The day of the week is never read. If you need one, write a check: it is an
128
+ object with an `id` and a `run`, it composes in the order you choose, and the trace will
129
+ show it firing beside the built-in ones.
130
+
115
131
  Order is a design decision and it should be visible. Consent has to come before
116
132
  everything. Quiet hours have to come before the budget, or a rejected candidate
117
133
  consumes a delivery it never made. Reorder freely; the trace will show what you did.
@@ -208,20 +224,67 @@ Both ship off. They read numbers the caller puts on the candidate.
208
224
 
209
225
  - `utilityFloor({ costFalseAlarm, costMissedHelp })` acts only when `candidate.pAccept` clears
210
226
  `tau = cFA / (cFA + pNeed * cFN)` (`pNeed` defaults to 1) and skips when there is no
211
- `pAccept`. This is Horvitz's expected-utility rule with the PRISM threshold.
227
+ `pAccept`. That threshold is the classical Bayes decision boundary: alerting costs
228
+ `(1 - p) * cFA`, silence costs `p * cFN`, so you alert when the first is the smaller.
229
+ The alerting application is [Horvitz, Jacobs and Hovel, "Attention-Sensitive Alerting",
230
+ UAI 1999](https://arxiv.org/abs/1301.6707), whose system is named Priorities.
212
231
  - `boundedDeferral({ lambda, interruptCost, staleness, boundSeconds })` never rejects. When
213
232
  `candidate.busy` is true it moves `deliverAt` to `now + t*`, with
214
233
  `t* = min(bound, lambda * interruptCost / (2 * staleness))`; the defaults give 116 seconds.
215
234
 
216
235
  Neither check ships a model, a cost or a probability. `costFalseAlarm`, `costMissedHelp`,
217
236
  `interruptCost` and `staleness` are yours to measure, and the package has no opinion about
218
- what an interruption costs your users. The rules come from Eric Horvitz's work on
219
- attention-sensitive alerting and bounded deferral; the field measurement people usually
220
- reach for is [Iqbal and Horvitz, "Disruption and recovery of computing tasks", CHI
237
+ what an interruption costs your users. The field measurement people usually reach for is
238
+ [Iqbal and Horvitz, "Disruption and recovery of computing tasks", CHI
221
239
  2007](https://erichorvitz.com/CHI_2007_Iqbal_Horvitz.pdf), which logged real users and put
222
240
  the return to a suspended task in the region of 11 to 16 minutes. The widely repeated "23
223
241
  minutes 15 seconds" figure is not from a peer-reviewed paper and is not used here.
224
242
 
243
+ `boundedDeferral` implements the derivation in [Achlioptas and Horvitz, "Principles of
244
+ Bounded Deferral for Balancing Information Awareness with
245
+ Interruption"](http://erichorvitz.com/Bounded_Deferral.pdf): expected cost is stationary
246
+ where `f'(t0) = lambda * c`, so a quadratic staleness `f(t) = s * t²` gives
247
+ `t* = lambda * c / (2 * s)`.
248
+
249
+ ## Which defaults are measured and which are ours
250
+
251
+ Every default here is either taken from a study, which is then named, or chosen by
252
+ judgement, which is then admitted. There is one of the first kind.
253
+
254
+ | default | where it comes from |
255
+ |---|---|
256
+ | `lambda = 1/43` in `boundedDeferral` | Measured. Achlioptas and Horvitz above: 113 Microsoft employees (42 program managers, 25 developers, 19 testers, 10 administrators, 9 managers, 4 in sales and marketing, 4 research scientists), three sequential business days between 10am and 4pm, 4,803 busy situations, mean busy session 43.12 s, standard deviation 51.79 s |
257
+ | `staleness = 0.0001`, `boundSeconds = 240` | Scale choices. Only the ratio `interruptCost / staleness` changes `t*`, so this pair is one way to write "a few minutes". Nothing fixes either number |
258
+ | `trustRamp` 7 days | Ours. No study sets it |
259
+ | `dismissalCooldown` 3 in 30 days buying 7 days | Ours. A dismissal is the clearest signal a user gives, so the shape is defensible; the three numbers are not from anywhere |
260
+ | `dailyBudget` 5 | Ours, in a supported direction. Pielot and Rello (below) cite an in-situ log study where participants received a median of 63.5 notifications a day, so a handful sits far below the ambient load. Nothing in that work says five |
261
+
262
+ The spread inside the one measured number is worth more than the number. The same paper's
263
+ two-subject analysis puts the mean time to a lower-cost state after an alert at 11 seconds
264
+ for one person and 101 seconds for the other, so the variation between two people is larger
265
+ than the default itself. Measure your own users before you trust it.
266
+
267
+ ### Deferring is supported; silence is not free
268
+
269
+ The strongest evidence that deferral works at all is [Okoshi, Tsubouchi and Tokuda,
270
+ "Real-world large-scale study on adaptive notification scheduling on smartphones",
271
+ *Pervasive and Mobile Computing* 50:1-24
272
+ (2018)](https://keio.elsevierpure.com/en/publications/real-world-large-scale-study-on-adaptive-notification-scheduling-/):
273
+ the Yahoo! JAPAN Android app, more than 680,000 users over three weeks, where holding a
274
+ notification until an interruptible moment was detected cut response time by 49.7 percent
275
+ against immediate delivery. That supports the direction. It says nothing about any window,
276
+ budget or cooldown in this package.
277
+
278
+ The counterweight belongs here too, because a gate that suppresses is not free. In [Pielot
279
+ and Rello, "Productive, Anxious, Lonely: 24 Hours Without Push Notifications", MobileHCI
280
+ 2017](https://arxiv.org/abs/1612.02314), 30 volunteers switched notifications off for a day.
281
+ They were less distracted, and they also worried about missing information, checked their
282
+ phones more often, and felt less connected to the people around them. Fifteen of the thirty
283
+ agreed they were afraid of missing something urgent. Three people approached for the study
284
+ refused outright, because their workplace expected them to be reachable. A silence your user
285
+ did not choose costs them something, and that cost does not appear in any trace this library
286
+ prints.
287
+
225
288
  ## Presets: platform quotas and legal limits, with sources
226
289
 
227
290
  ```ts
@@ -250,6 +313,25 @@ Each preset carries `sources` (the pages the numbers come from) and a `note` on
250
313
  out. Reviewable defaults, not legal advice: several official sources disagree with each other,
251
314
  and the note says which value was chosen and why.
252
315
 
316
+ **Read the scope before you reach for a legal preset.** Every instrument above regulates
317
+ *commercial* communication. `usTcpa`, `euEprivacy`, `krNetworkAct50` and `jpAntiSpamLaw` are
318
+ marketing rules, so they bind your message only when the message itself is commercial. A
319
+ reminder your user asked for is not advertising, and pulling in a marketing preset for it
320
+ imports a restriction the law never placed on you, which is its own kind of wrong answer.
321
+ Use them when the candidate is promotional; when it is not, the platform quotas and your own
322
+ quiet hours are the honest constraints.
323
+
324
+ That scope test is also why some jurisdictions people ask for are missing. Canada's CASL and
325
+ Australia's Spam Act 2003 set consent, identification and unsubscribe duties, and neither
326
+ carries a time-of-day rule at all. The Brazilian window quoted around the web comes from bill
327
+ PLS 48/2018, a proposal rather than enacted law, and it covers telemarketing calls. India is
328
+ the interesting one: the widely repeated "9am to 9pm" is not what the primary text says. The
329
+ Telecom Commercial Communications Customer Preference Regulations make time bands a
330
+ *preference the subscriber registers* with their access provider, alongside content category
331
+ and day type, not a fixed statutory quiet window, and the secondary sources that quote a
332
+ window disagree with each other about whether it starts at 09:00 or 10:00. A preset built on
333
+ that would encode a number no primary source states, so there is none.
334
+
253
335
  ## The budget is enforced at commit, not at evaluate
254
336
 
255
337
  Two instances can both evaluate a candidate for the same user, both see four of
package/README.tr.md CHANGED
@@ -182,10 +182,67 @@ bir birim tüketmez.
182
182
 
183
183
  - `utilityFloor({ costFalseAlarm, costMissedHelp })` yalnızca `candidate.pAccept` değeri
184
184
  `tau = cFA / (cFA + pNeed * cFN)` eşiğini geçtiğinde konuşur (`pNeed` varsayılanı 1);
185
- `pAccept` yoksa atlar. Bu, Horvitz'in beklenen fayda kuralı ve PRISM eşiğidir.
185
+ `pAccept` yoksa atlar. Bu eşik klasik Bayes karar sınırıdır: konuşmanın maliyeti
186
+ `(1 - p) * cFA`, susmanın maliyeti `p * cFN`, hangisi küçükse o seçilir. Uyarı alanındaki
187
+ karşılığı [Horvitz, Jacobs ve Hovel, "Attention-Sensitive Alerting", UAI
188
+ 1999](https://arxiv.org/abs/1301.6707); o makaledeki sistemin adı Priorities.
186
189
  - `boundedDeferral({ lambda, interruptCost, staleness, boundSeconds })` asla reddetmez.
187
190
  `candidate.busy` doğruysa `deliverAt` değerini `now + t*` yapar;
188
191
  `t* = min(bound, lambda * interruptCost / (2 * staleness))`, varsayılanlar 116 saniye verir.
192
+ Türetim [Achlioptas ve Horvitz, "Principles of Bounded
193
+ Deferral"](http://erichorvitz.com/Bounded_Deferral.pdf) makalesinden.
194
+
195
+ ## Hangi varsayılan ölçüldü, hangisi bizim tercihimiz
196
+
197
+ Buradaki her varsayılan ya bir çalışmadan geliyor ve kaynağı yazılıyor, ya da bir kanaat ve
198
+ bunu söylüyoruz. Birinci türden tek bir tane var.
199
+
200
+ | varsayılan | nereden geliyor |
201
+ |---|---|
202
+ | `boundedDeferral` içindeki `lambda = 1/43` | Ölçüm. Yukarıdaki makale: 113 çalışan, üç ardışık iş günü, 10.00 ile 16.00 arası, 4.803 meşgul durum, ortalama meşguliyet süresi 43,12 saniye, standart sapma 51,79 saniye |
203
+ | `staleness = 0.0001`, `boundSeconds = 240` | Ölçek tercihi. `t*` yalnızca `interruptCost / staleness` oranına bağlı; bu çift "birkaç dakika" demenin bir yolu, iki sayıyı da sabitleyen bir bulgu yok |
204
+ | `trustRamp` 7 gün | Bizim. Hiçbir çalışma bu sayıyı vermiyor |
205
+ | `dismissalCooldown` 30 günde 3 kapatma, 7 gün sessizlik | Bizim. Kapatma, kullanıcının verdiği en net sinyal olduğu için biçim savunulabilir; üç sayı bize ait |
206
+ | `dailyBudget` 5 | Bizim, ama yönü destekli. Pielot ve Rello'nun aktardığı yerinde günlük kayıt çalışmasında katılımcılar günde ortanca 63,5 bildirim alıyor; bir avuç mesaj bunun çok altında. O çalışma "beş" demiyor |
207
+
208
+ Ölçülen tek sayının içindeki dağılım, sayının kendisinden değerli: aynı makalenin iki kişilik
209
+ çözümlemesinde uyarı sonrası düşük maliyetli duruma geçiş ortalaması birinde 11, diğerinde
210
+ 101 saniye. İki kişi arasındaki fark varsayılanın kendisinden büyük.
211
+
212
+ ### Ertelemenin dayanağı var, susmanın bedeli de var
213
+
214
+ Ertelemenin işe yaradığına dair en güçlü kanıt [Okoshi, Tsubouchi ve Tokuda, *Pervasive and
215
+ Mobile Computing* 50:1-24
216
+ (2018)](https://keio.elsevierpure.com/en/publications/real-world-large-scale-study-on-adaptive-notification-scheduling-/):
217
+ Yahoo! JAPAN Android uygulaması, 680.000'den fazla kullanıcı, üç hafta; bildirimi uygun ana
218
+ kadar bekletmek yanıt süresini yüzde 49,7 kısaltmış. Bu, yönü destekler; bu paketteki
219
+ hiçbir pencereyi, bütçeyi veya bekleme süresini desteklemez.
220
+
221
+ Karşı ağırlık da burada durmalı, çünkü susturan bir kapı bedelsiz değil. [Pielot ve Rello,
222
+ MobileHCI 2017](https://arxiv.org/abs/1612.02314) çalışmasında 30 gönüllü bir gün boyunca
223
+ bildirimleri kapatmış. Daha az dağılmışlar, ama aynı zamanda bir şeyi kaçırmaktan
224
+ endişelenmiş, telefonlarına daha sık bakmış ve çevrelerinden kopuk hissetmişler. Otuz kişiden
225
+ on beşi acil bir şeyi kaçırmaktan korktuğunu söylemiş. Çalışma için görüşülen üç kişi,
226
+ işyerinde sürekli ulaşılabilir olmaları beklendiği için katılmayı reddetmiş. Kullanıcının
227
+ seçmediği bir sessizliğin bir bedeli var ve o bedel bu kütüphanenin yazdığı hiçbir izde
228
+ görünmüyor.
229
+
230
+ ## Benimsemeden önce bilmeniz gereken iki sınır
231
+
232
+ İkisi de hata değil ve ikisi de testle sabitlendi, böylece ileride değişecekse bilerek değişir.
233
+
234
+ **Hafta, ISO haftasıdır; haftalık bütçe pazartesi yenilenir.** Çalışma haftası pazardan
235
+ perşembeye uzanan yerlerde bu yenilenme haftanın birinci gününe denk gelir: pazar günü
236
+ bütçesini harcayan bir kullanıcı pazartesi sabahı bütçesini geri alır ve önünde hâlâ dört
237
+ iş günü vardır. Anahtarı değiştirmek deponuzdaki bütün sayaçları kaydıracağı için bunu
238
+ sessizce değiştirmek yerine yazıyoruz. ISO haftası sizin kullanıcılarınız için yanlışsa
239
+ kendi bütçe kontrolünüzü istediğiniz anahtarla yazabilirsiniz.
240
+
241
+ **Sessiz saatler tek bir penceredir ve haftanın her günü aynıdır.** Kullanıcıda tek bir
242
+ `start` ve tek bir `end` vardır; cuma penceresi, Şabat penceresi veya resmî tatil
243
+ tanımlanamaz. Haftanın günü hiç okunmaz. Böyle bir kurala ihtiyacınız varsa kendi
244
+ kontrolünüzü yazın: `id` ve `run` taşıyan bir nesnedir, istediğiniz sırada dizilir ve izde
245
+ yerleşik kontrollerin yanında görünür.
189
246
 
190
247
  ## Hazır paketler: platform kotaları ve yasal sınırlar, kaynaklarıyla
191
248
 
@@ -215,6 +272,24 @@ Her paket `sources` (sayıların geldiği sayfalar) ve neyi dışarıda bırakt
215
272
  `note` taşır. Gözden geçirilebilir varsayılanlar, hukuki tavsiye değil: birkaç resmi kaynak
216
273
  birbiriyle çelişir ve not hangi değerin neden seçildiğini söyler.
217
274
 
275
+ **Yasal bir pakete uzanmadan önce kapsamını okuyun.** Yukarıdaki bütün düzenlemeler *ticari*
276
+ iletişimi düzenler. `usTcpa`, `euEprivacy`, `krNetworkAct50` ve `jpAntiSpamLaw` birer pazarlama
277
+ kuralıdır; yani mesajınızı ancak mesajın kendisi ticari olduğunda bağlar. Kullanıcının kendi
278
+ istediği bir hatırlatma reklam değildir ve onun için pazarlama paketi kullanmak, yasanın size
279
+ hiç koymadığı bir kısıtı kendi elinizle içeri almak olur. Aday promosyon niteliğindeyse
280
+ kullanın; değilse dürüst sınırlar platform kotaları ve kendi sessiz saatlerinizdir.
281
+
282
+ Bazı ülkelerin neden burada olmadığı da aynı kapsam sınavıyla açıklanır. Kanada'nın CASL'i ve
283
+ Avustralya'nın 2003 tarihli Spam Act'i rıza, gönderen kimliği ve abonelikten çıkma
284
+ yükümlülükleri getirir; ikisinde de saat kısıtı yoktur. İnternette dolaşan Brezilya penceresi
285
+ PLS 48/2018 sayılı kanun *teklifinden* gelir, yürürlükteki bir kanundan değil, ve
286
+ telefonla pazarlama aramalarını kapsar. Hindistan ilginç olanı: sıkça tekrarlanan "09.00-21.00"
287
+ birincil metinde yazmaz. TRAI düzenlemesi zaman bantlarını, içerik kategorisi ve gün tipiyle
288
+ birlikte, abonenin operatörüne *kaydettirdiği bir tercih* yapar; sabit bir yasal sessizlik
289
+ penceresi değildir. Üstelik pencereyi aktaran ikincil kaynaklar başlangıcın 09.00 mı 10.00 mı
290
+ olduğunda birbiriyle çelişir. Bunun üzerine kurulacak bir paket, hiçbir birincil kaynağın
291
+ yazmadığı bir sayıyı kodlardı; o yüzden yok.
292
+
218
293
  ## Adaptörler
219
294
 
220
295
  | alt yol | framework | kapı nerede durur |
@@ -36,6 +36,9 @@ export declare function quietHours(options?: {
36
36
  * For the first `days` after sign-up the user hears from the system only at
37
37
  * or above `minPriority`. A proactive assistant is least calibrated exactly
38
38
  * when the user is least forgiving.
39
+ *
40
+ * Seven days is a judgement, not a finding. No study sets this number, and
41
+ * none of the literature the package cites speaks to it.
39
42
  */
40
43
  export declare function trustRamp(options?: {
41
44
  days?: number;
@@ -45,6 +48,10 @@ export declare function trustRamp(options?: {
45
48
  * When the user has dismissed `dismissals` candidates of a type within
46
49
  * `withinDays`, that type stays silent for `silenceDays`. Fed by
47
50
  * gate.record(userId, candidate, "dismissed").
51
+ *
52
+ * Three in thirty buying seven days is a judgement, not a finding. The shape
53
+ * is defensible, since a dismissal is the clearest signal a user gives; the
54
+ * three numbers are ours and no study sets them.
48
55
  */
49
56
  export declare function dismissalCooldown(options?: {
50
57
  dismissals?: number;
@@ -74,15 +81,36 @@ export interface BudgetOptions {
74
81
  export declare const budgetKey: (userId: string, now: Date, timezone?: string) => string;
75
82
  export declare const weeklyBudgetKey: (userId: string, now: Date, timezone?: string) => string;
76
83
  export declare const monthlyBudgetKey: (userId: string, now: Date, timezone?: string) => string;
77
- /** At most `limit` deliveries per user per local day. */
84
+ /**
85
+ * At most `limit` deliveries per user per local day.
86
+ *
87
+ * Five is a judgement, not a finding. The direction has support: Pielot and
88
+ * Rello, "Productive, Anxious, Lonely: 24 Hours Without Push Notifications",
89
+ * MobileHCI 2017 (https://arxiv.org/abs/1612.02314), cite an in-situ log study
90
+ * (Pielot, Church and de Oliveira, MobileHCI 2014) in which participants
91
+ * received a median of 63.5 notifications a day, so a handful is far below the
92
+ * ambient load. Nothing in that work says five.
93
+ */
78
94
  export declare function dailyBudget(options?: BudgetOptions): BudgetCheck;
79
- /** At most `limit` deliveries per user per local ISO week. */
95
+ /**
96
+ * At most `limit` deliveries per user per local ISO week.
97
+ *
98
+ * The week is the ISO week, so the counter resets on Monday morning in the
99
+ * user's zone. For a Sunday-to-Thursday working week that reset lands
100
+ * mid-week. Documented rather than fixed; changing it would move every
101
+ * existing key.
102
+ */
80
103
  export declare function weeklyBudget(options?: BudgetOptions): BudgetCheck;
81
104
  /** At most `limit` deliveries per user per local calendar month. */
82
105
  export declare function monthlyBudget(options?: BudgetOptions): BudgetCheck;
83
106
  /**
84
- * Horvitz's expected-utility rule with the PRISM threshold: act only when the
85
- * caller's estimate of acceptance clears tau = cFA / (cFA + pNeed * cFN).
107
+ * Expected-utility alerting: act only when the caller's estimate of acceptance
108
+ * clears tau = cFA / (cFA + pNeed * cFN). That threshold is the classical Bayes
109
+ * decision boundary between the cost of alerting when the user did not want it,
110
+ * (1 - p) * cFA, and the cost of staying silent when they did, p * cFN.
111
+ * The alerting application is Horvitz, Jacobs and Hovel, "Attention-Sensitive
112
+ * Alerting", UAI 1999 (https://arxiv.org/abs/1301.6707); the system in that
113
+ * paper is named Priorities.
86
114
  * `candidate.pAccept` and `candidate.pNeed` come from the caller's own model.
87
115
  */
88
116
  export declare function utilityFloor(options: {
@@ -90,10 +118,29 @@ export declare function utilityFloor(options: {
90
118
  costMissedHelp: number;
91
119
  }): Check;
92
120
  /**
93
- * Bounded deferral (Horvitz): when the user is busy, wait t* = min(bound,
121
+ * Bounded deferral: when the user is busy, wait t* = min(bound,
94
122
  * lambda * interruptCost / (2 * staleness)), the optimum of a quadratic
95
123
  * staleness loss against the cost of interrupting a busy person, with the
96
124
  * user becoming free at rate lambda. Never rejects; only moves deliverAt.
125
+ *
126
+ * The derivation is Achlioptas and Horvitz, "Principles of Bounded Deferral
127
+ * for Balancing Information Awareness with Interruption", Microsoft Research
128
+ * (http://erichorvitz.com/Bounded_Deferral.pdf): the expected cost is
129
+ * stationary where f'(t0) = lambda * c with f''(t0) > 0, so a quadratic
130
+ * staleness f(t) = s * t^2 gives t* = lambda * c / (2 * s).
131
+ *
132
+ * `lambda` defaults to 1/43 from the same paper's field study: 113 Microsoft
133
+ * employees (42 program managers, 25 developers, 19 testers, 10 administrators,
134
+ * 9 managers, 4 in sales and marketing, 4 research scientists), three
135
+ * sequential business days between 10am and 4pm, 4,803 busy situations, mean
136
+ * busy-session duration 43.12 s with a standard deviation of 51.79 s. That
137
+ * spread matters: the same paper's two-subject Interruption Workbench analysis
138
+ * puts the mean time to a lower-cost state after an alert at 11 s for one
139
+ * person and 101 s for the other. Measure your own users before trusting it.
140
+ *
141
+ * `staleness` and `boundSeconds` are scale choices, not findings. Only the
142
+ * ratio interruptCost / staleness affects t*, so the pair below is one way to
143
+ * express "a few minutes"; nothing in the literature fixes either number.
97
144
  */
98
145
  export declare function boundedDeferral(options?: {
99
146
  lambda?: number;
@@ -132,6 +132,9 @@ export function quietHours(options = {}) {
132
132
  * For the first `days` after sign-up the user hears from the system only at
133
133
  * or above `minPriority`. A proactive assistant is least calibrated exactly
134
134
  * when the user is least forgiving.
135
+ *
136
+ * Seven days is a judgement, not a finding. No study sets this number, and
137
+ * none of the literature the package cites speaks to it.
135
138
  */
136
139
  export function trustRamp(options = {}) {
137
140
  const days = options.days ?? 7;
@@ -153,6 +156,10 @@ export function trustRamp(options = {}) {
153
156
  * When the user has dismissed `dismissals` candidates of a type within
154
157
  * `withinDays`, that type stays silent for `silenceDays`. Fed by
155
158
  * gate.record(userId, candidate, "dismissed").
159
+ *
160
+ * Three in thirty buying seven days is a judgement, not a finding. The shape
161
+ * is defensible, since a dismissal is the clearest signal a user gives; the
162
+ * three numbers are ours and no study sets them.
156
163
  */
157
164
  export function dismissalCooldown(options = {}) {
158
165
  const n = options.dismissals ?? 3;
@@ -235,11 +242,27 @@ const isoWeekKey = (day) => {
235
242
  };
236
243
  export const weeklyBudgetKey = (userId, now, timezone) => `weeklyBudget:${userId}:${isoWeekKey(localDay(now, timezone))}`;
237
244
  export const monthlyBudgetKey = (userId, now, timezone) => `monthlyBudget:${userId}:${localDay(now, timezone).slice(0, 7)}`;
238
- /** At most `limit` deliveries per user per local day. */
245
+ /**
246
+ * At most `limit` deliveries per user per local day.
247
+ *
248
+ * Five is a judgement, not a finding. The direction has support: Pielot and
249
+ * Rello, "Productive, Anxious, Lonely: 24 Hours Without Push Notifications",
250
+ * MobileHCI 2017 (https://arxiv.org/abs/1612.02314), cite an in-situ log study
251
+ * (Pielot, Church and de Oliveira, MobileHCI 2014) in which participants
252
+ * received a median of 63.5 notifications a day, so a handful is far below the
253
+ * ambient load. Nothing in that work says five.
254
+ */
239
255
  export function dailyBudget(options = {}) {
240
256
  return budget({ id: "dailyBudget", label: "daily budget", defaultLimit: 5, keyFor: ({ user, now }) => budgetKey(user.id, now, user.timezone), ttlSeconds: 2 * DAY_SECONDS }, options);
241
257
  }
242
- /** At most `limit` deliveries per user per local ISO week. */
258
+ /**
259
+ * At most `limit` deliveries per user per local ISO week.
260
+ *
261
+ * The week is the ISO week, so the counter resets on Monday morning in the
262
+ * user's zone. For a Sunday-to-Thursday working week that reset lands
263
+ * mid-week. Documented rather than fixed; changing it would move every
264
+ * existing key.
265
+ */
243
266
  export function weeklyBudget(options = {}) {
244
267
  return budget({ id: "weeklyBudget", label: "weekly budget", defaultLimit: 20, keyFor: ({ user, now }) => weeklyBudgetKey(user.id, now, user.timezone), ttlSeconds: 8 * DAY_SECONDS }, options);
245
268
  }
@@ -251,8 +274,13 @@ export function monthlyBudget(options = {}) {
251
274
  /* Optional, caller-fed checks. Off by default; the package ships no model. */
252
275
  /* ------------------------------------------------------------------------ */
253
276
  /**
254
- * Horvitz's expected-utility rule with the PRISM threshold: act only when the
255
- * caller's estimate of acceptance clears tau = cFA / (cFA + pNeed * cFN).
277
+ * Expected-utility alerting: act only when the caller's estimate of acceptance
278
+ * clears tau = cFA / (cFA + pNeed * cFN). That threshold is the classical Bayes
279
+ * decision boundary between the cost of alerting when the user did not want it,
280
+ * (1 - p) * cFA, and the cost of staying silent when they did, p * cFN.
281
+ * The alerting application is Horvitz, Jacobs and Hovel, "Attention-Sensitive
282
+ * Alerting", UAI 1999 (https://arxiv.org/abs/1301.6707); the system in that
283
+ * paper is named Priorities.
256
284
  * `candidate.pAccept` and `candidate.pNeed` come from the caller's own model.
257
285
  */
258
286
  export function utilityFloor(options) {
@@ -270,10 +298,29 @@ export function utilityFloor(options) {
270
298
  }
271
299
  const round3 = (n) => Math.round(n * 1000) / 1000;
272
300
  /**
273
- * Bounded deferral (Horvitz): when the user is busy, wait t* = min(bound,
301
+ * Bounded deferral: when the user is busy, wait t* = min(bound,
274
302
  * lambda * interruptCost / (2 * staleness)), the optimum of a quadratic
275
303
  * staleness loss against the cost of interrupting a busy person, with the
276
304
  * user becoming free at rate lambda. Never rejects; only moves deliverAt.
305
+ *
306
+ * The derivation is Achlioptas and Horvitz, "Principles of Bounded Deferral
307
+ * for Balancing Information Awareness with Interruption", Microsoft Research
308
+ * (http://erichorvitz.com/Bounded_Deferral.pdf): the expected cost is
309
+ * stationary where f'(t0) = lambda * c with f''(t0) > 0, so a quadratic
310
+ * staleness f(t) = s * t^2 gives t* = lambda * c / (2 * s).
311
+ *
312
+ * `lambda` defaults to 1/43 from the same paper's field study: 113 Microsoft
313
+ * employees (42 program managers, 25 developers, 19 testers, 10 administrators,
314
+ * 9 managers, 4 in sales and marketing, 4 research scientists), three
315
+ * sequential business days between 10am and 4pm, 4,803 busy situations, mean
316
+ * busy-session duration 43.12 s with a standard deviation of 51.79 s. That
317
+ * spread matters: the same paper's two-subject Interruption Workbench analysis
318
+ * puts the mean time to a lower-cost state after an alert at 11 s for one
319
+ * person and 101 s for the other. Measure your own users before trusting it.
320
+ *
321
+ * `staleness` and `boundSeconds` are scale choices, not findings. Only the
322
+ * ratio interruptCost / staleness affects t*, so the pair below is one way to
323
+ * express "a few minutes"; nothing in the literature fixes either number.
277
324
  */
278
325
  export function boundedDeferral(options = {}) {
279
326
  const lambda = options.lambda ?? 1 / 43;
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "proactive-gate",
3
- "version": "0.2.0",
3
+ "version": "0.2.1",
4
4
  "description": "Decide whether a proactive AI agent may reach a user right now, and log why not. Ordered checks as code or JSON, a conformance spec, presets for platform and legal limits, adapters for AI SDK, Mastra, LangChain and OpenAI Agents, and a Python sibling.",
5
5
  "type": "module",
6
6
  "main": "./dist/src/index.js",