proactive-gate 0.2.0 → 0.2.2
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 +116 -5
- package/README.tr.md +80 -1
- package/dist/src/checks.d.ts +77 -6
- package/dist/src/checks.js +114 -10
- package/dist/src/types.d.ts +40 -5
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -100,18 +100,59 @@ something returned false".
|
|
|
100
100
|
| 5 | `snooze()` | `user.snoozedUntil` is in the future | global pause |
|
|
101
101
|
| 6 | `mute()` | `candidate.type` is in `user.mutedTypes` | per-type mute |
|
|
102
102
|
| 7 | `intensity()` | priority is below the user's intensity floor | low hears only high, normal hears normal and up, high hears everything |
|
|
103
|
-
| 8 | `quietHours({ priorityFloor })` | inside the user's local quiet window | IANA time zone, window may cross midnight, bypassed at or above the floor |
|
|
103
|
+
| 8 | `quietHours({ priorityFloor })` | inside the user's local quiet window | IANA time zone, window may cross midnight, bypassed at or above the floor; one window every day or [a schedule per day](#quiet-hours-that-differ-by-day) |
|
|
104
104
|
| 9 | `trustRamp({ days, minPriority })` | user is newer than `days` and priority is below the floor | the system is least calibrated exactly when the user is least forgiving |
|
|
105
105
|
| 10 | `dismissalCooldown({ dismissals, withinDays, silenceDays })` | the user dismissed that type `dismissals` times in the window | fed by `gate.record(user, candidate, "dismissed")`; every further dismissal restarts the silence |
|
|
106
106
|
| 11 | `adaptiveTiming({ nextGoodMoment, surfacesFor })` | never | non-rejecting: moves `deliverAt` or narrows surfaces; a check marked `nonRejecting` cannot reject even if it tries |
|
|
107
107
|
| 12 | `dailyBudget({ limit, bypassPriority })` | the user's local-day counter is at the limit | `evaluate` reads, `commit` increments atomically and can still refuse |
|
|
108
108
|
|
|
109
|
+
### Quiet hours that differ by day
|
|
110
|
+
|
|
111
|
+
A working week is not Monday to Friday everywhere, and a holiday is not a weekday at all.
|
|
112
|
+
`quietHours` takes a schedule as well as a single window:
|
|
113
|
+
|
|
114
|
+
```ts
|
|
115
|
+
quietHours: {
|
|
116
|
+
default: { start: "22:00", end: "08:00" },
|
|
117
|
+
days: { fri: { start: "00:00", end: "23:59" }, sat: { start: "00:00", end: "23:59" }, sun: null },
|
|
118
|
+
dates: { "2026-12-25": { start: "00:00", end: "23:59" } },
|
|
119
|
+
}
|
|
120
|
+
```
|
|
121
|
+
|
|
122
|
+
A date beats a weekday beats the default, and `null` means the day has no quiet hours, which is
|
|
123
|
+
how a working day is carved out of a default. A window belongs to the day it opens on, so one
|
|
124
|
+
that crosses midnight silences the next morning and the reason names the day it came from.
|
|
125
|
+
|
|
126
|
+
Two things this deliberately does not do. There is no bundled holiday calendar: the dates you
|
|
127
|
+
observe are yours to supply, and a bundled one goes stale without anyone noticing. And one row
|
|
128
|
+
cannot express more than 24 hours, so a Friday evening to Saturday evening silence is two rows,
|
|
129
|
+
`fri: 18:00 to 00:00` and `sat: 00:00 to 20:00`.
|
|
130
|
+
|
|
131
|
+
Passing a single window is unchanged and remains the common case; a schedule whose every day
|
|
132
|
+
resolves to the same window behaves identically to that window.
|
|
133
|
+
|
|
109
134
|
`weeklyBudget({ limit, bypassPriority })` is the same shape keyed on the user's local ISO
|
|
110
135
|
week; `defaultChecks({ weeklyLimit })` places it just before the daily one. Budgets are
|
|
111
136
|
consumed in check order at commit, so when a weekly check passes and the daily one then
|
|
112
137
|
refuses, that weekly unit is spent without a delivery. It only happens when two commits
|
|
113
138
|
race after a shared evaluate.
|
|
114
139
|
|
|
140
|
+
### Two limits you should know before you adopt this
|
|
141
|
+
|
|
142
|
+
Neither is a bug, and both are pinned by tests so a future change has to be deliberate.
|
|
143
|
+
|
|
144
|
+
**The week is the ISO week, so the weekly budget refills on Monday.** Where the working
|
|
145
|
+
week runs Sunday to Thursday, that refill lands one day in: a user who spends the budget on
|
|
146
|
+
Sunday has it back on Monday, with four working days still to run. Changing the key would
|
|
147
|
+
move every counter already in your store, so it is documented rather than quietly altered.
|
|
148
|
+
Pass your own budget check keyed how you like if the ISO week is wrong for your users.
|
|
149
|
+
|
|
150
|
+
**Quiet hours are a single window, the same on every day of the week.** A user carries one
|
|
151
|
+
`start` and one `end`, so a Friday window, a Shabbat window or a public holiday cannot be
|
|
152
|
+
expressed. The day of the week is never read. If you need one, write a check: it is an
|
|
153
|
+
object with an `id` and a `run`, it composes in the order you choose, and the trace will
|
|
154
|
+
show it firing beside the built-in ones.
|
|
155
|
+
|
|
115
156
|
Order is a design decision and it should be visible. Consent has to come before
|
|
116
157
|
everything. Quiet hours have to come before the budget, or a rejected candidate
|
|
117
158
|
consumes a delivery it never made. Reorder freely; the trace will show what you did.
|
|
@@ -208,20 +249,67 @@ Both ship off. They read numbers the caller puts on the candidate.
|
|
|
208
249
|
|
|
209
250
|
- `utilityFloor({ costFalseAlarm, costMissedHelp })` acts only when `candidate.pAccept` clears
|
|
210
251
|
`tau = cFA / (cFA + pNeed * cFN)` (`pNeed` defaults to 1) and skips when there is no
|
|
211
|
-
`pAccept`.
|
|
252
|
+
`pAccept`. That threshold is the classical Bayes decision boundary: alerting costs
|
|
253
|
+
`(1 - p) * cFA`, silence costs `p * cFN`, so you alert when the first is the smaller.
|
|
254
|
+
The alerting application is [Horvitz, Jacobs and Hovel, "Attention-Sensitive Alerting",
|
|
255
|
+
UAI 1999](https://arxiv.org/abs/1301.6707), whose system is named Priorities.
|
|
212
256
|
- `boundedDeferral({ lambda, interruptCost, staleness, boundSeconds })` never rejects. When
|
|
213
257
|
`candidate.busy` is true it moves `deliverAt` to `now + t*`, with
|
|
214
258
|
`t* = min(bound, lambda * interruptCost / (2 * staleness))`; the defaults give 116 seconds.
|
|
215
259
|
|
|
216
260
|
Neither check ships a model, a cost or a probability. `costFalseAlarm`, `costMissedHelp`,
|
|
217
261
|
`interruptCost` and `staleness` are yours to measure, and the package has no opinion about
|
|
218
|
-
what an interruption costs your users. The
|
|
219
|
-
|
|
220
|
-
reach for is [Iqbal and Horvitz, "Disruption and recovery of computing tasks", CHI
|
|
262
|
+
what an interruption costs your users. The field measurement people usually reach for is
|
|
263
|
+
[Iqbal and Horvitz, "Disruption and recovery of computing tasks", CHI
|
|
221
264
|
2007](https://erichorvitz.com/CHI_2007_Iqbal_Horvitz.pdf), which logged real users and put
|
|
222
265
|
the return to a suspended task in the region of 11 to 16 minutes. The widely repeated "23
|
|
223
266
|
minutes 15 seconds" figure is not from a peer-reviewed paper and is not used here.
|
|
224
267
|
|
|
268
|
+
`boundedDeferral` implements the derivation in [Achlioptas and Horvitz, "Principles of
|
|
269
|
+
Bounded Deferral for Balancing Information Awareness with
|
|
270
|
+
Interruption"](http://erichorvitz.com/Bounded_Deferral.pdf): expected cost is stationary
|
|
271
|
+
where `f'(t0) = lambda * c`, so a quadratic staleness `f(t) = s * t²` gives
|
|
272
|
+
`t* = lambda * c / (2 * s)`.
|
|
273
|
+
|
|
274
|
+
## Which defaults are measured and which are ours
|
|
275
|
+
|
|
276
|
+
Every default here is either taken from a study, which is then named, or chosen by
|
|
277
|
+
judgement, which is then admitted. There is one of the first kind.
|
|
278
|
+
|
|
279
|
+
| default | where it comes from |
|
|
280
|
+
|---|---|
|
|
281
|
+
| `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 |
|
|
282
|
+
| `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 |
|
|
283
|
+
| `trustRamp` 7 days | Ours. No study sets it |
|
|
284
|
+
| `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 |
|
|
285
|
+
| `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 |
|
|
286
|
+
|
|
287
|
+
The spread inside the one measured number is worth more than the number. The same paper's
|
|
288
|
+
two-subject analysis puts the mean time to a lower-cost state after an alert at 11 seconds
|
|
289
|
+
for one person and 101 seconds for the other, so the variation between two people is larger
|
|
290
|
+
than the default itself. Measure your own users before you trust it.
|
|
291
|
+
|
|
292
|
+
### Deferring is supported; silence is not free
|
|
293
|
+
|
|
294
|
+
The strongest evidence that deferral works at all is [Okoshi, Tsubouchi and Tokuda,
|
|
295
|
+
"Real-world large-scale study on adaptive notification scheduling on smartphones",
|
|
296
|
+
*Pervasive and Mobile Computing* 50:1-24
|
|
297
|
+
(2018)](https://keio.elsevierpure.com/en/publications/real-world-large-scale-study-on-adaptive-notification-scheduling-/):
|
|
298
|
+
the Yahoo! JAPAN Android app, more than 680,000 users over three weeks, where holding a
|
|
299
|
+
notification until an interruptible moment was detected cut response time by 49.7 percent
|
|
300
|
+
against immediate delivery. That supports the direction. It says nothing about any window,
|
|
301
|
+
budget or cooldown in this package.
|
|
302
|
+
|
|
303
|
+
The counterweight belongs here too, because a gate that suppresses is not free. In [Pielot
|
|
304
|
+
and Rello, "Productive, Anxious, Lonely: 24 Hours Without Push Notifications", MobileHCI
|
|
305
|
+
2017](https://arxiv.org/abs/1612.02314), 30 volunteers switched notifications off for a day.
|
|
306
|
+
They were less distracted, and they also worried about missing information, checked their
|
|
307
|
+
phones more often, and felt less connected to the people around them. Fifteen of the thirty
|
|
308
|
+
agreed they were afraid of missing something urgent. Three people approached for the study
|
|
309
|
+
refused outright, because their workplace expected them to be reachable. A silence your user
|
|
310
|
+
did not choose costs them something, and that cost does not appear in any trace this library
|
|
311
|
+
prints.
|
|
312
|
+
|
|
225
313
|
## Presets: platform quotas and legal limits, with sources
|
|
226
314
|
|
|
227
315
|
```ts
|
|
@@ -250,6 +338,25 @@ Each preset carries `sources` (the pages the numbers come from) and a `note` on
|
|
|
250
338
|
out. Reviewable defaults, not legal advice: several official sources disagree with each other,
|
|
251
339
|
and the note says which value was chosen and why.
|
|
252
340
|
|
|
341
|
+
**Read the scope before you reach for a legal preset.** Every instrument above regulates
|
|
342
|
+
*commercial* communication. `usTcpa`, `euEprivacy`, `krNetworkAct50` and `jpAntiSpamLaw` are
|
|
343
|
+
marketing rules, so they bind your message only when the message itself is commercial. A
|
|
344
|
+
reminder your user asked for is not advertising, and pulling in a marketing preset for it
|
|
345
|
+
imports a restriction the law never placed on you, which is its own kind of wrong answer.
|
|
346
|
+
Use them when the candidate is promotional; when it is not, the platform quotas and your own
|
|
347
|
+
quiet hours are the honest constraints.
|
|
348
|
+
|
|
349
|
+
That scope test is also why some jurisdictions people ask for are missing. Canada's CASL and
|
|
350
|
+
Australia's Spam Act 2003 set consent, identification and unsubscribe duties, and neither
|
|
351
|
+
carries a time-of-day rule at all. The Brazilian window quoted around the web comes from bill
|
|
352
|
+
PLS 48/2018, a proposal rather than enacted law, and it covers telemarketing calls. India is
|
|
353
|
+
the interesting one: the widely repeated "9am to 9pm" is not what the primary text says. The
|
|
354
|
+
Telecom Commercial Communications Customer Preference Regulations make time bands a
|
|
355
|
+
*preference the subscriber registers* with their access provider, alongside content category
|
|
356
|
+
and day type, not a fixed statutory quiet window, and the secondary sources that quote a
|
|
357
|
+
window disagree with each other about whether it starts at 09:00 or 10:00. A preset built on
|
|
358
|
+
that would encode a number no primary source states, so there is none.
|
|
359
|
+
|
|
253
360
|
## The budget is enforced at commit, not at evaluate
|
|
254
361
|
|
|
255
362
|
Two instances can both evaluate a candidate for the same user, both see four of
|
|
@@ -390,6 +497,10 @@ preset) still refuses. Both are part of `npm run examples` and of the test suite
|
|
|
390
497
|
pip install proactive-gate
|
|
391
498
|
```
|
|
392
499
|
|
|
500
|
+
To run an unreleased state, install from the repository instead: `pip install "proactive-gate @
|
|
501
|
+
git+https://github.com/Bubblegunn/proactive-gate#subdirectory=python"`. The published release was
|
|
502
|
+
uploaded from a local build with a token, so unlike the npm package it carries no build provenance.
|
|
503
|
+
|
|
393
504
|
```python
|
|
394
505
|
from proactive_gate import Gate
|
|
395
506
|
gate = Gate.from_policy(policy) # the same policy.json
|
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
|
|
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 |
|
|
@@ -234,6 +309,10 @@ kurmak gerekmez. Her biri kapının gerekçesiyle reddeder ve onayda bütçeyi t
|
|
|
234
309
|
pip install proactive-gate
|
|
235
310
|
```
|
|
236
311
|
|
|
312
|
+
Yayınlanmamış bir durumu denemek için depodan kurulur: `pip install "proactive-gate @
|
|
313
|
+
git+https://github.com/Bubblegunn/proactive-gate#subdirectory=python"`. Yayınlanan sürüm yerel bir
|
|
314
|
+
derlemeden token ile yüklendi; npm paketinin aksine derleme kanıtı taşımıyor.
|
|
315
|
+
|
|
237
316
|
`python/` sapan bir port değil, bir kardeştir: `spec/fixtures` altındaki her senaryoyu senkron
|
|
238
317
|
`Gate` ve `AsyncGate` (Redis, `redis.asyncio` üzerinden) ile geçer; mypy strict, CI'da Python
|
|
239
318
|
3.11 ve 3.13. Bkz. [`python/README.md`](python/README.md).
|
package/dist/src/checks.d.ts
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import type { Check, CheckContext, Priority, Surface } from "./types.js";
|
|
1
|
+
import type { Check, CheckContext, Priority, QuietSchedule, QuietWindow, Surface, Weekday } from "./types.js";
|
|
2
2
|
export declare const DAY_SECONDS: number;
|
|
3
3
|
/** Local "HH:MM" and calendar day for an instant in an IANA zone, using Intl only. */
|
|
4
4
|
export declare function localClock(now: Date, timezone: string): {
|
|
@@ -7,6 +7,30 @@ export declare function localClock(now: Date, timezone: string): {
|
|
|
7
7
|
};
|
|
8
8
|
/** True when `minutes` falls inside [start, end), where the window may cross midnight. */
|
|
9
9
|
export declare function inWindow(minutes: number, start: number, end: number): boolean;
|
|
10
|
+
/**
|
|
11
|
+
* The weekday of a local calendar date, and the date before it.
|
|
12
|
+
*
|
|
13
|
+
* Both are pure calendar arithmetic on the "YYYY-MM-DD" that `localClock` already
|
|
14
|
+
* resolved through Intl, never arithmetic on an instant. That is what keeps zones
|
|
15
|
+
* with a 45-minute offset (Kathmandu, Chatham, Eucla) and every daylight-saving
|
|
16
|
+
* transition out of this: the offset was applied before we got here.
|
|
17
|
+
*/
|
|
18
|
+
export declare function weekdayOf(day: string): Weekday;
|
|
19
|
+
export declare function dayBefore(day: string): string;
|
|
20
|
+
/** The window in force on one local date: a date beats a weekday beats the default. */
|
|
21
|
+
export declare function windowFor(quiet: QuietWindow | QuietSchedule, day: string): QuietWindow | null;
|
|
22
|
+
/**
|
|
23
|
+
* Whether a local time is inside quiet hours, and which day's window says so.
|
|
24
|
+
*
|
|
25
|
+
* A window that crosses midnight belongs to the day it opens on, so a time can be
|
|
26
|
+
* quiet because of yesterday: Friday 18:00 to 00:00 silences Saturday 00:00 too.
|
|
27
|
+
* With one window every day this reduces exactly to `inWindow`, which is why the
|
|
28
|
+
* single-window form keeps behaving as it did.
|
|
29
|
+
*/
|
|
30
|
+
export declare function quietAt(quiet: QuietWindow | QuietSchedule, day: string, minutes: number): {
|
|
31
|
+
window: QuietWindow;
|
|
32
|
+
day: string;
|
|
33
|
+
} | null;
|
|
10
34
|
/** A production hard-stop that silences every producer at once. */
|
|
11
35
|
export declare function killSwitch(isOn: () => boolean | Promise<boolean>): Check;
|
|
12
36
|
/** Consent comes before everything, or you have evaluated preferences for someone who never agreed. */
|
|
@@ -36,6 +60,9 @@ export declare function quietHours(options?: {
|
|
|
36
60
|
* For the first `days` after sign-up the user hears from the system only at
|
|
37
61
|
* or above `minPriority`. A proactive assistant is least calibrated exactly
|
|
38
62
|
* when the user is least forgiving.
|
|
63
|
+
*
|
|
64
|
+
* Seven days is a judgement, not a finding. No study sets this number, and
|
|
65
|
+
* none of the literature the package cites speaks to it.
|
|
39
66
|
*/
|
|
40
67
|
export declare function trustRamp(options?: {
|
|
41
68
|
days?: number;
|
|
@@ -45,6 +72,10 @@ export declare function trustRamp(options?: {
|
|
|
45
72
|
* When the user has dismissed `dismissals` candidates of a type within
|
|
46
73
|
* `withinDays`, that type stays silent for `silenceDays`. Fed by
|
|
47
74
|
* gate.record(userId, candidate, "dismissed").
|
|
75
|
+
*
|
|
76
|
+
* Three in thirty buying seven days is a judgement, not a finding. The shape
|
|
77
|
+
* is defensible, since a dismissal is the clearest signal a user gives; the
|
|
78
|
+
* three numbers are ours and no study sets them.
|
|
48
79
|
*/
|
|
49
80
|
export declare function dismissalCooldown(options?: {
|
|
50
81
|
dismissals?: number;
|
|
@@ -74,15 +105,36 @@ export interface BudgetOptions {
|
|
|
74
105
|
export declare const budgetKey: (userId: string, now: Date, timezone?: string) => string;
|
|
75
106
|
export declare const weeklyBudgetKey: (userId: string, now: Date, timezone?: string) => string;
|
|
76
107
|
export declare const monthlyBudgetKey: (userId: string, now: Date, timezone?: string) => string;
|
|
77
|
-
/**
|
|
108
|
+
/**
|
|
109
|
+
* At most `limit` deliveries per user per local day.
|
|
110
|
+
*
|
|
111
|
+
* Five is a judgement, not a finding. The direction has support: Pielot and
|
|
112
|
+
* Rello, "Productive, Anxious, Lonely: 24 Hours Without Push Notifications",
|
|
113
|
+
* MobileHCI 2017 (https://arxiv.org/abs/1612.02314), cite an in-situ log study
|
|
114
|
+
* (Pielot, Church and de Oliveira, MobileHCI 2014) in which participants
|
|
115
|
+
* received a median of 63.5 notifications a day, so a handful is far below the
|
|
116
|
+
* ambient load. Nothing in that work says five.
|
|
117
|
+
*/
|
|
78
118
|
export declare function dailyBudget(options?: BudgetOptions): BudgetCheck;
|
|
79
|
-
/**
|
|
119
|
+
/**
|
|
120
|
+
* At most `limit` deliveries per user per local ISO week.
|
|
121
|
+
*
|
|
122
|
+
* The week is the ISO week, so the counter resets on Monday morning in the
|
|
123
|
+
* user's zone. For a Sunday-to-Thursday working week that reset lands
|
|
124
|
+
* mid-week. Documented rather than fixed; changing it would move every
|
|
125
|
+
* existing key.
|
|
126
|
+
*/
|
|
80
127
|
export declare function weeklyBudget(options?: BudgetOptions): BudgetCheck;
|
|
81
128
|
/** At most `limit` deliveries per user per local calendar month. */
|
|
82
129
|
export declare function monthlyBudget(options?: BudgetOptions): BudgetCheck;
|
|
83
130
|
/**
|
|
84
|
-
*
|
|
85
|
-
*
|
|
131
|
+
* Expected-utility alerting: act only when the caller's estimate of acceptance
|
|
132
|
+
* clears tau = cFA / (cFA + pNeed * cFN). That threshold is the classical Bayes
|
|
133
|
+
* decision boundary between the cost of alerting when the user did not want it,
|
|
134
|
+
* (1 - p) * cFA, and the cost of staying silent when they did, p * cFN.
|
|
135
|
+
* The alerting application is Horvitz, Jacobs and Hovel, "Attention-Sensitive
|
|
136
|
+
* Alerting", UAI 1999 (https://arxiv.org/abs/1301.6707); the system in that
|
|
137
|
+
* paper is named Priorities.
|
|
86
138
|
* `candidate.pAccept` and `candidate.pNeed` come from the caller's own model.
|
|
87
139
|
*/
|
|
88
140
|
export declare function utilityFloor(options: {
|
|
@@ -90,10 +142,29 @@ export declare function utilityFloor(options: {
|
|
|
90
142
|
costMissedHelp: number;
|
|
91
143
|
}): Check;
|
|
92
144
|
/**
|
|
93
|
-
* Bounded deferral
|
|
145
|
+
* Bounded deferral: when the user is busy, wait t* = min(bound,
|
|
94
146
|
* lambda * interruptCost / (2 * staleness)), the optimum of a quadratic
|
|
95
147
|
* staleness loss against the cost of interrupting a busy person, with the
|
|
96
148
|
* user becoming free at rate lambda. Never rejects; only moves deliverAt.
|
|
149
|
+
*
|
|
150
|
+
* The derivation is Achlioptas and Horvitz, "Principles of Bounded Deferral
|
|
151
|
+
* for Balancing Information Awareness with Interruption", Microsoft Research
|
|
152
|
+
* (http://erichorvitz.com/Bounded_Deferral.pdf): the expected cost is
|
|
153
|
+
* stationary where f'(t0) = lambda * c with f''(t0) > 0, so a quadratic
|
|
154
|
+
* staleness f(t) = s * t^2 gives t* = lambda * c / (2 * s).
|
|
155
|
+
*
|
|
156
|
+
* `lambda` defaults to 1/43 from the same paper's field study: 113 Microsoft
|
|
157
|
+
* employees (42 program managers, 25 developers, 19 testers, 10 administrators,
|
|
158
|
+
* 9 managers, 4 in sales and marketing, 4 research scientists), three
|
|
159
|
+
* sequential business days between 10am and 4pm, 4,803 busy situations, mean
|
|
160
|
+
* busy-session duration 43.12 s with a standard deviation of 51.79 s. That
|
|
161
|
+
* spread matters: the same paper's two-subject Interruption Workbench analysis
|
|
162
|
+
* puts the mean time to a lower-cost state after an alert at 11 s for one
|
|
163
|
+
* person and 101 s for the other. Measure your own users before trusting it.
|
|
164
|
+
*
|
|
165
|
+
* `staleness` and `boundSeconds` are scale choices, not findings. Only the
|
|
166
|
+
* ratio interruptCost / staleness affects t*, so the pair below is one way to
|
|
167
|
+
* express "a few minutes"; nothing in the literature fixes either number.
|
|
97
168
|
*/
|
|
98
169
|
export declare function boundedDeferral(options?: {
|
|
99
170
|
lambda?: number;
|
package/dist/src/checks.js
CHANGED
|
@@ -39,6 +39,61 @@ export function inWindow(minutes, start, end) {
|
|
|
39
39
|
return start < end ? minutes >= start && minutes < end : minutes >= start || minutes < end;
|
|
40
40
|
}
|
|
41
41
|
const localDay = (now, timezone) => (timezone ? localClock(now, timezone).day : now.toISOString().slice(0, 10));
|
|
42
|
+
const WEEKDAYS = ["sun", "mon", "tue", "wed", "thu", "fri", "sat"];
|
|
43
|
+
/**
|
|
44
|
+
* The weekday of a local calendar date, and the date before it.
|
|
45
|
+
*
|
|
46
|
+
* Both are pure calendar arithmetic on the "YYYY-MM-DD" that `localClock` already
|
|
47
|
+
* resolved through Intl, never arithmetic on an instant. That is what keeps zones
|
|
48
|
+
* with a 45-minute offset (Kathmandu, Chatham, Eucla) and every daylight-saving
|
|
49
|
+
* transition out of this: the offset was applied before we got here.
|
|
50
|
+
*/
|
|
51
|
+
export function weekdayOf(day) {
|
|
52
|
+
const [y, m, d] = day.split("-").map(Number);
|
|
53
|
+
return WEEKDAYS[new Date(Date.UTC(y, m - 1, d)).getUTCDay()];
|
|
54
|
+
}
|
|
55
|
+
export function dayBefore(day) {
|
|
56
|
+
const [y, m, d] = day.split("-").map(Number);
|
|
57
|
+
return new Date(Date.UTC(y, m - 1, d - 1)).toISOString().slice(0, 10);
|
|
58
|
+
}
|
|
59
|
+
const isSchedule = (q) => !("start" in q);
|
|
60
|
+
/** The window in force on one local date: a date beats a weekday beats the default. */
|
|
61
|
+
export function windowFor(quiet, day) {
|
|
62
|
+
if (!isSchedule(quiet))
|
|
63
|
+
return quiet;
|
|
64
|
+
const byDate = quiet.dates?.[day];
|
|
65
|
+
if (byDate !== undefined)
|
|
66
|
+
return byDate;
|
|
67
|
+
const byDay = quiet.days?.[weekdayOf(day)];
|
|
68
|
+
if (byDay !== undefined)
|
|
69
|
+
return byDay;
|
|
70
|
+
return quiet.default ?? null;
|
|
71
|
+
}
|
|
72
|
+
/**
|
|
73
|
+
* Whether a local time is inside quiet hours, and which day's window says so.
|
|
74
|
+
*
|
|
75
|
+
* A window that crosses midnight belongs to the day it opens on, so a time can be
|
|
76
|
+
* quiet because of yesterday: Friday 18:00 to 00:00 silences Saturday 00:00 too.
|
|
77
|
+
* With one window every day this reduces exactly to `inWindow`, which is why the
|
|
78
|
+
* single-window form keeps behaving as it did.
|
|
79
|
+
*/
|
|
80
|
+
export function quietAt(quiet, day, minutes) {
|
|
81
|
+
const today = windowFor(quiet, day);
|
|
82
|
+
if (today) {
|
|
83
|
+
const start = parseHHMM(today.start);
|
|
84
|
+
const end = parseHHMM(today.end);
|
|
85
|
+
if (start !== end && (start < end ? minutes >= start && minutes < end : minutes >= start))
|
|
86
|
+
return { window: today, day };
|
|
87
|
+
}
|
|
88
|
+
const yesterday = windowFor(quiet, dayBefore(day));
|
|
89
|
+
if (yesterday) {
|
|
90
|
+
const start = parseHHMM(yesterday.start);
|
|
91
|
+
const end = parseHHMM(yesterday.end);
|
|
92
|
+
if (start > end && minutes < end)
|
|
93
|
+
return { window: yesterday, day: dayBefore(day) };
|
|
94
|
+
}
|
|
95
|
+
return null;
|
|
96
|
+
}
|
|
42
97
|
/* ------------------------------------------------------------------------ */
|
|
43
98
|
/* The checks, in the order LILA runs them. Compose your own order freely. */
|
|
44
99
|
/* ------------------------------------------------------------------------ */
|
|
@@ -117,14 +172,16 @@ export function quietHours(options = {}) {
|
|
|
117
172
|
return pass;
|
|
118
173
|
if (!user.timezone)
|
|
119
174
|
return skip("quiet hours set but no timezone on the user; cannot evaluate");
|
|
120
|
-
const { minutes } = localClock(now, user.timezone);
|
|
121
|
-
const
|
|
122
|
-
|
|
123
|
-
if (!inWindow(minutes, start, end))
|
|
175
|
+
const { minutes, day } = localClock(now, user.timezone);
|
|
176
|
+
const hit = quietAt(user.quietHours, day, minutes);
|
|
177
|
+
if (!hit)
|
|
124
178
|
return pass;
|
|
125
179
|
if (atLeast(priority, floor))
|
|
126
180
|
return pass;
|
|
127
|
-
|
|
181
|
+
// Name the day the window came from: when it crossed midnight the reason is
|
|
182
|
+
// yesterday's setting, and a reader looking at today's would not find it.
|
|
183
|
+
const whose = hit.day === day ? "" : ` (${weekdayOf(hit.day)} ${hit.day})`;
|
|
184
|
+
return reject(`quiet hours ${hit.window.start} to ${hit.window.end}${whose} ${user.timezone}; priority ${priority} is below the floor (${floor})`);
|
|
128
185
|
},
|
|
129
186
|
};
|
|
130
187
|
}
|
|
@@ -132,6 +189,9 @@ export function quietHours(options = {}) {
|
|
|
132
189
|
* For the first `days` after sign-up the user hears from the system only at
|
|
133
190
|
* or above `minPriority`. A proactive assistant is least calibrated exactly
|
|
134
191
|
* when the user is least forgiving.
|
|
192
|
+
*
|
|
193
|
+
* Seven days is a judgement, not a finding. No study sets this number, and
|
|
194
|
+
* none of the literature the package cites speaks to it.
|
|
135
195
|
*/
|
|
136
196
|
export function trustRamp(options = {}) {
|
|
137
197
|
const days = options.days ?? 7;
|
|
@@ -153,6 +213,10 @@ export function trustRamp(options = {}) {
|
|
|
153
213
|
* When the user has dismissed `dismissals` candidates of a type within
|
|
154
214
|
* `withinDays`, that type stays silent for `silenceDays`. Fed by
|
|
155
215
|
* gate.record(userId, candidate, "dismissed").
|
|
216
|
+
*
|
|
217
|
+
* Three in thirty buying seven days is a judgement, not a finding. The shape
|
|
218
|
+
* is defensible, since a dismissal is the clearest signal a user gives; the
|
|
219
|
+
* three numbers are ours and no study sets them.
|
|
156
220
|
*/
|
|
157
221
|
export function dismissalCooldown(options = {}) {
|
|
158
222
|
const n = options.dismissals ?? 3;
|
|
@@ -235,11 +299,27 @@ const isoWeekKey = (day) => {
|
|
|
235
299
|
};
|
|
236
300
|
export const weeklyBudgetKey = (userId, now, timezone) => `weeklyBudget:${userId}:${isoWeekKey(localDay(now, timezone))}`;
|
|
237
301
|
export const monthlyBudgetKey = (userId, now, timezone) => `monthlyBudget:${userId}:${localDay(now, timezone).slice(0, 7)}`;
|
|
238
|
-
/**
|
|
302
|
+
/**
|
|
303
|
+
* At most `limit` deliveries per user per local day.
|
|
304
|
+
*
|
|
305
|
+
* Five is a judgement, not a finding. The direction has support: Pielot and
|
|
306
|
+
* Rello, "Productive, Anxious, Lonely: 24 Hours Without Push Notifications",
|
|
307
|
+
* MobileHCI 2017 (https://arxiv.org/abs/1612.02314), cite an in-situ log study
|
|
308
|
+
* (Pielot, Church and de Oliveira, MobileHCI 2014) in which participants
|
|
309
|
+
* received a median of 63.5 notifications a day, so a handful is far below the
|
|
310
|
+
* ambient load. Nothing in that work says five.
|
|
311
|
+
*/
|
|
239
312
|
export function dailyBudget(options = {}) {
|
|
240
313
|
return budget({ id: "dailyBudget", label: "daily budget", defaultLimit: 5, keyFor: ({ user, now }) => budgetKey(user.id, now, user.timezone), ttlSeconds: 2 * DAY_SECONDS }, options);
|
|
241
314
|
}
|
|
242
|
-
/**
|
|
315
|
+
/**
|
|
316
|
+
* At most `limit` deliveries per user per local ISO week.
|
|
317
|
+
*
|
|
318
|
+
* The week is the ISO week, so the counter resets on Monday morning in the
|
|
319
|
+
* user's zone. For a Sunday-to-Thursday working week that reset lands
|
|
320
|
+
* mid-week. Documented rather than fixed; changing it would move every
|
|
321
|
+
* existing key.
|
|
322
|
+
*/
|
|
243
323
|
export function weeklyBudget(options = {}) {
|
|
244
324
|
return budget({ id: "weeklyBudget", label: "weekly budget", defaultLimit: 20, keyFor: ({ user, now }) => weeklyBudgetKey(user.id, now, user.timezone), ttlSeconds: 8 * DAY_SECONDS }, options);
|
|
245
325
|
}
|
|
@@ -251,8 +331,13 @@ export function monthlyBudget(options = {}) {
|
|
|
251
331
|
/* Optional, caller-fed checks. Off by default; the package ships no model. */
|
|
252
332
|
/* ------------------------------------------------------------------------ */
|
|
253
333
|
/**
|
|
254
|
-
*
|
|
255
|
-
*
|
|
334
|
+
* Expected-utility alerting: act only when the caller's estimate of acceptance
|
|
335
|
+
* clears tau = cFA / (cFA + pNeed * cFN). That threshold is the classical Bayes
|
|
336
|
+
* decision boundary between the cost of alerting when the user did not want it,
|
|
337
|
+
* (1 - p) * cFA, and the cost of staying silent when they did, p * cFN.
|
|
338
|
+
* The alerting application is Horvitz, Jacobs and Hovel, "Attention-Sensitive
|
|
339
|
+
* Alerting", UAI 1999 (https://arxiv.org/abs/1301.6707); the system in that
|
|
340
|
+
* paper is named Priorities.
|
|
256
341
|
* `candidate.pAccept` and `candidate.pNeed` come from the caller's own model.
|
|
257
342
|
*/
|
|
258
343
|
export function utilityFloor(options) {
|
|
@@ -270,10 +355,29 @@ export function utilityFloor(options) {
|
|
|
270
355
|
}
|
|
271
356
|
const round3 = (n) => Math.round(n * 1000) / 1000;
|
|
272
357
|
/**
|
|
273
|
-
* Bounded deferral
|
|
358
|
+
* Bounded deferral: when the user is busy, wait t* = min(bound,
|
|
274
359
|
* lambda * interruptCost / (2 * staleness)), the optimum of a quadratic
|
|
275
360
|
* staleness loss against the cost of interrupting a busy person, with the
|
|
276
361
|
* user becoming free at rate lambda. Never rejects; only moves deliverAt.
|
|
362
|
+
*
|
|
363
|
+
* The derivation is Achlioptas and Horvitz, "Principles of Bounded Deferral
|
|
364
|
+
* for Balancing Information Awareness with Interruption", Microsoft Research
|
|
365
|
+
* (http://erichorvitz.com/Bounded_Deferral.pdf): the expected cost is
|
|
366
|
+
* stationary where f'(t0) = lambda * c with f''(t0) > 0, so a quadratic
|
|
367
|
+
* staleness f(t) = s * t^2 gives t* = lambda * c / (2 * s).
|
|
368
|
+
*
|
|
369
|
+
* `lambda` defaults to 1/43 from the same paper's field study: 113 Microsoft
|
|
370
|
+
* employees (42 program managers, 25 developers, 19 testers, 10 administrators,
|
|
371
|
+
* 9 managers, 4 in sales and marketing, 4 research scientists), three
|
|
372
|
+
* sequential business days between 10am and 4pm, 4,803 busy situations, mean
|
|
373
|
+
* busy-session duration 43.12 s with a standard deviation of 51.79 s. That
|
|
374
|
+
* spread matters: the same paper's two-subject Interruption Workbench analysis
|
|
375
|
+
* puts the mean time to a lower-cost state after an alert at 11 s for one
|
|
376
|
+
* person and 101 s for the other. Measure your own users before trusting it.
|
|
377
|
+
*
|
|
378
|
+
* `staleness` and `boundSeconds` are scale choices, not findings. Only the
|
|
379
|
+
* ratio interruptCost / staleness affects t*, so the pair below is one way to
|
|
380
|
+
* express "a few minutes"; nothing in the literature fixes either number.
|
|
277
381
|
*/
|
|
278
382
|
export function boundedDeferral(options = {}) {
|
|
279
383
|
const lambda = options.lambda ?? 1 / 43;
|
package/dist/src/types.d.ts
CHANGED
|
@@ -3,6 +3,27 @@ export type Priority = "low" | "normal" | "high" | "critical";
|
|
|
3
3
|
export declare const PRIORITY_RANK: Record<Priority, number>;
|
|
4
4
|
/** Where a delivery may land. Free-form so callers can add their own. */
|
|
5
5
|
export type Surface = "feed" | "push" | "chat" | "voice" | "email" | (string & {});
|
|
6
|
+
/** A quiet window in local time, "HH:MM" to "HH:MM". `start` after `end` crosses midnight. */
|
|
7
|
+
export type QuietWindow = {
|
|
8
|
+
start: string;
|
|
9
|
+
end: string;
|
|
10
|
+
};
|
|
11
|
+
/** Weekday keys for a quiet-hours schedule, Sunday first to match `Date#getUTCDay`. */
|
|
12
|
+
export type Weekday = "sun" | "mon" | "tue" | "wed" | "thu" | "fri" | "sat";
|
|
13
|
+
/**
|
|
14
|
+
* Quiet hours that differ by day. A working week is not Monday to Friday everywhere,
|
|
15
|
+
* and a holiday is not a weekday at all, so the window is resolved per day: a calendar
|
|
16
|
+
* date first, then the weekday, then the default. `null` at any level means the day has
|
|
17
|
+
* no quiet hours.
|
|
18
|
+
*
|
|
19
|
+
* There is no bundled holiday calendar and there will not be one: the dates a caller
|
|
20
|
+
* observes are the caller's to supply, and a bundled calendar goes stale silently.
|
|
21
|
+
*/
|
|
22
|
+
export type QuietSchedule = {
|
|
23
|
+
default?: QuietWindow | null;
|
|
24
|
+
days?: Partial<Record<Weekday, QuietWindow | null>>;
|
|
25
|
+
dates?: Record<string, QuietWindow | null>;
|
|
26
|
+
};
|
|
6
27
|
/** Everything the gate knows about the person it might interrupt. */
|
|
7
28
|
export interface UserState {
|
|
8
29
|
id: string;
|
|
@@ -20,11 +41,25 @@ export interface UserState {
|
|
|
20
41
|
intensity?: "low" | "normal" | "high";
|
|
21
42
|
/** IANA time zone, required for quiet hours. */
|
|
22
43
|
timezone?: string;
|
|
23
|
-
/**
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
44
|
+
/**
|
|
45
|
+
* Quiet hours in local time, "HH:MM". May cross midnight.
|
|
46
|
+
*
|
|
47
|
+
* One window applies every day. A schedule gives a window per weekday, and per
|
|
48
|
+
* calendar date for the days a weekday cannot express, such as a public holiday:
|
|
49
|
+
*
|
|
50
|
+
* ```ts
|
|
51
|
+
* quietHours: {
|
|
52
|
+
* default: { start: "22:00", end: "08:00" },
|
|
53
|
+
* days: { fri: { start: "18:00", end: "00:00" }, sat: { start: "00:00", end: "20:00" } },
|
|
54
|
+
* dates: { "2026-12-25": { start: "00:00", end: "23:59" } },
|
|
55
|
+
* }
|
|
56
|
+
* ```
|
|
57
|
+
*
|
|
58
|
+
* `null` for a weekday or a date means no quiet hours that day, which is how you
|
|
59
|
+
* carve a working day out of a default. A date beats a weekday, a weekday beats
|
|
60
|
+
* the default. Dates are the user's local calendar dates, "YYYY-MM-DD".
|
|
61
|
+
*/
|
|
62
|
+
quietHours?: QuietWindow | QuietSchedule | null;
|
|
28
63
|
/** When the user joined. Drives the trust ramp. */
|
|
29
64
|
createdAt?: Date | string;
|
|
30
65
|
/** Surfaces the user allows, in preference order. Defaults to the candidate's surfaces. */
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "proactive-gate",
|
|
3
|
-
"version": "0.2.
|
|
3
|
+
"version": "0.2.2",
|
|
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",
|