proactive-gate 0.3.0 → 0.3.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
@@ -4,10 +4,8 @@ English | [Türkçe](README.tr.md)
4
4
 
5
5
  <p>
6
6
  <img src="https://img.shields.io/npm/v/proactive-gate?style=flat-square&color=111111&label=npm" alt="npm">
7
- <img src="https://img.shields.io/npm/dm/proactive-gate?style=flat-square&color=111111" alt="npm downloads">
8
7
  <img src="https://img.shields.io/github/actions/workflow/status/Bubblegunn/proactive-gate/ci.yml?style=flat-square&color=111111&label=ci" alt="ci">
9
8
  <img src="https://img.shields.io/bundlephobia/minzip/proactive-gate?style=flat-square&color=111111" alt="minzipped size">
10
- <img src="https://img.shields.io/github/stars/Bubblegunn/proactive-gate?style=flat-square&color=111111" alt="stars">
11
9
  <img src="https://img.shields.io/badge/license-MIT-111111?style=flat-square" alt="MIT">
12
10
  <a href="https://doi.org/10.5281/zenodo.22393512"><img src="https://img.shields.io/badge/DOI-10.5281%2Fzenodo.22393512-111111?style=flat-square" alt="DOI"></a>
13
11
  </p>
@@ -49,6 +47,13 @@ preset you named, and prints the preset's own source next to the few lines that
49
47
  the gate into that framework. `npx proactive-gate init --list` shows the fourteen
50
48
  platform and legal presets and the four frameworks.
51
49
 
50
+ If you would rather see the argument than read it: `npm run bench:compare` replays a
51
+ committed day through this gate and through five hand-written `if` statements, and prints
52
+ the six places they disagree: a critical alert the cap should have let through, a two-day-old
53
+ account, a snooze, a run of dismissals, and a local day boundary that silences one user and
54
+ pays another twice. The output, and why none of the six is a matter of taste, is in
55
+ [Compared with hand-rolled checks](#compared-with-hand-rolled-checks-and-feature-flags).
56
+
52
57
  Zero dependencies. TypeScript. Node 20 or newer. Framework-agnostic: the gate sits
53
58
  between "the model produced something" and "the user's phone buzzed", whichever
54
59
  model or framework produced it. Examples: [`examples/vercel-ai-sdk.ts`](examples/vercel-ai-sdk.ts),
@@ -433,8 +438,12 @@ Set `windowSeconds` from your own transport's retry horizon.
433
438
  `MemoryStore` keeps values in process memory and is useful for a single instance. `RedisStore`
434
439
  shares values across instances. `SqliteStore` persists values in a SQLite database without
435
440
  adding a package dependency. It was contributed by
436
- [@aaqib-hafeez-khan-in](https://github.com/aaqib-hafeez-khan-in) in [#3](https://github.com/Bubblegunn/proactive-gate/pull/3). `SqliteStore` requires Node.js 22.5 or newer; the SQLite module
441
+ [@Aaqibhafeezkhan](https://github.com/Aaqibhafeezkhan) in [#3](https://github.com/Bubblegunn/proactive-gate/pull/3). `SqliteStore` requires Node.js 22.5 or newer; the SQLite module
437
442
  is loaded only when the store is constructed so the package can still be used on Node.js 20. On Node 22 the module prints an ExperimentalWarning on first use; it is stable from Node 24.
443
+ Expired rows are removed when a read touches them, and every `set` or `incr` first removes the
444
+ rows that have already expired, so a key nobody reads again still disappears: a year of daily
445
+ budget keys leaves the live rows in the table rather than one dead row per day. The write-time sweep and its partial index were contributed by
446
+ [@LouisDeconinck](https://github.com/LouisDeconinck) in [#27](https://github.com/Bubblegunn/proactive-gate/pull/27).
438
447
 
439
448
  **Writing your own store?** `proactive-gate/store-contract` exports the same suite these three are
440
449
  held to, so you can prove yours behaves rather than hope:
@@ -448,7 +457,7 @@ It checks `get`, `set` and `del`, `incr` from an absent key, concurrent `incr` a
448
457
  expiry boundary, and that a TTL given to `set` and one given to `incr` agree, then replays a
449
458
  seeded random operation sequence against `MemoryStore`. A store whose backend owns the clock
450
459
  passes `expiry: "skip"` and those cases are reported as skipped rather than quietly dropped. It
451
- was contributed by [@aaqib-hafeez-khan-in](https://github.com/aaqib-hafeez-khan-in) in
460
+ was contributed by [@Aaqibhafeezkhan](https://github.com/Aaqibhafeezkhan) in
452
461
  [#24](https://github.com/Bubblegunn/proactive-gate/pull/24), and lives on its own subpath so
453
462
  importing the package never pulls `node:test` into your bundle. See
454
463
  [docs/store-contract.md](docs/store-contract.md).
@@ -563,6 +572,15 @@ candidates with the clock taken from each line, and `node examples/ai-sdk/run.mj
563
572
  day of tool-approval requests, one of which is a critical alert that a legal window (the TCPA
564
573
  preset) still refuses. Both are part of `npm run examples` and of the test suite.
565
574
 
575
+ The other four, the `.ts` files, are illustrations rather than fixtures: they import
576
+ `@langchain/langgraph`, `@mastra/core` and the AI SDK, none of which is a dependency here, so
577
+ they are neither compiled nor executed and this README does not claim they are. What *is*
578
+ checked is the half we control: `test/example-imports.test.mjs` asserts that every symbol they
579
+ import from `proactive-gate` still exists, as a value or as a type, so renaming an export
580
+ cannot leave a published snippet quietly telling readers to import something that is gone. It
581
+ cannot tell you a framework changed its own API, and installing four agent frameworks to
582
+ type-check four snippets is a worse trade than saying which files are executed.
583
+
566
584
  ## Python
567
585
 
568
586
  ```
@@ -737,13 +755,15 @@ trace, and adds the part they leave out: the budget consumed at send time.
737
755
  ## Thanks
738
756
 
739
757
  Two people sent pull requests on the day this was published, neither of whom I had spoken to
740
- before. [@aaqib-hafeez-khan-in](https://github.com/aaqib-hafeez-khan-in) wrote `SqliteStore`
758
+ before. [@Aaqibhafeezkhan](https://github.com/Aaqibhafeezkhan) wrote `SqliteStore`
741
759
  ([#3](https://github.com/Bubblegunn/proactive-gate/pull/3)) and
742
760
  [@edwardsong08](https://github.com/edwardsong08) wrote the weekly budget
743
761
  ([#9](https://github.com/Bubblegunn/proactive-gate/pull/9)). Both shipped in 0.1.2 and are in
744
- every release since, including the one you install today. @aaqib-hafeez-khan-in came back for a
762
+ every release since, including the one you install today. @Aaqibhafeezkhan came back for a
745
763
  second one and wrote the store contract suite in [#24](https://github.com/Bubblegunn/proactive-gate/pull/24).
746
764
 
765
+ A third person arrived from the other direction. [@LouisDeconinck](https://github.com/LouisDeconinck) took [#26](https://github.com/Bubblegunn/proactive-gate/issues/26), an issue this project filed against itself to admit that expired rows were never cleaned up, and twelve minutes later sent the fix in both languages with the test that would have caught the original bug ([#27](https://github.com/Bubblegunn/proactive-gate/pull/27), in 0.3.1).
766
+
747
767
  ## Cite this
748
768
 
749
769
  Every release is archived on Zenodo with a DOI, so a paper or a report can point at the
package/README.tr.md CHANGED
@@ -37,6 +37,14 @@ Sıfır bağımlılık. TypeScript. Node 20 ya da üstü. Framework'ten bağıms
37
37
  [bubblegunn.github.io/proactive-gate](https://bubblegunn.github.io/proactive-gate/). Python:
38
38
  [`python/`](python/README.md).
39
39
 
40
+ Okumak yerine görmeyi tercih ederseniz: `npm run bench:compare`, kayıtlı bir günü hem bu kapıdan
41
+ hem de elle yazılmış beş `if` ifadesinden geçirir ve ayrıştıkları altı yeri yazdırır: kotanın
42
+ geçirmesi gereken kritik bir uyarı, iki günlük bir hesap, bir erteleme, arka arkaya gelen
43
+ reddedişler, ve bir kullanıcıyı susturup bir diğerine iki kat mesaj veren yerel gün sınırı. Altısının da
44
+ neden zevk meselesi olmadığı, bu dosyanın kısaltılmış olması nedeniyle yalnızca İngilizce
45
+ README'de anlatılıyor: [Compared with hand-rolled
46
+ checks](README.md#compared-with-hand-rolled-checks-and-feature-flags).
47
+
40
48
  ## Bir karar neye benzer
41
49
 
42
50
  ```ts
@@ -330,6 +338,16 @@ yazmadığı bir sayıyı kodlardı; o yüzden yok.
330
338
  Adaptörler framework paketine değil, çağrının biçimine göre tiplenmiştir; başka bir şey
331
339
  kurmak gerekmez. Her biri kapının gerekçesiyle reddeder ve onayda bütçeyi tüketir.
332
340
 
341
+ Örneklerden ikisi framework kurulu olmadan ve ağ olmadan çalışır: `node examples/mastra/run.mjs`
342
+ ve `node examples/ai-sdk/run.mjs`. İkisi de `npm run examples` ve test paketinin parçası.
343
+ Diğer dört `.ts` dosyası ise fikstür değil, örnekleme: `@langchain/langgraph`, `@mastra/core` ve
344
+ AI SDK'yı içe aktarıyorlar, hiçbiri buranın bağımlılığı değil, dolayısıyla ne derleniyor ne
345
+ çalıştırılıyorlar ve bu README onların çalıştığını iddia etmiyor. Denetlenen şey, bizim
346
+ denetleyebildiğimiz yarısı: `test/example-imports.test.mjs`, bu dosyaların `proactive-gate`'ten içe
347
+ aktardığı her sembolün değer ya da tip olarak hâlâ var olduğunu doğrular, böylece bir export'un adı
348
+ değiştiğinde yayımlanmış bir örnek okuyucuya artık var olmayan bir şeyi içe aktarmasını söylemeye
349
+ sessizce devam edemez.
350
+
333
351
  ## Python
334
352
 
335
353
  ```
@@ -337,8 +355,10 @@ pip install proactive-gate
337
355
  ```
338
356
 
339
357
  Yayınlanmamış bir durumu denemek için depodan kurulur: `pip install "proactive-gate @
340
- git+https://github.com/Bubblegunn/proactive-gate#subdirectory=python"`. Yayınlanan sürüm yerel bir
341
- derlemeden token ile yüklendi; npm paketinin aksine derleme kanıtı taşımıyor.
358
+ git+https://github.com/Bubblegunn/proactive-gate#subdirectory=python"`. Python paketi npm paketiyle
359
+ aynı workflow tarafından yayımlanıyor, dolayısıyla her dosyayı hangi deponun ve hangi workflow'un
360
+ ürettiğini adlandıran PyPI yayın attestation'ları taşıyor. 0.2.2 öncesi sürümler yerel bir
361
+ derlemeden token ile yüklendi ve hiçbir kanıt taşımıyor.
342
362
 
343
363
  `python/` sapan bir port değil, bir kardeştir: `spec/fixtures` altındaki her senaryoyu senkron
344
364
  `Gate` ve `AsyncGate` (Redis, `redis.asyncio` üzerinden) ile geçer; mypy strict, CI'da Python
@@ -40,10 +40,17 @@ export declare class SqliteStore implements Store {
40
40
  private readonly database;
41
41
  private readonly clock;
42
42
  constructor(path: string, clock?: () => number);
43
+ /**
44
+ * A read prunes only the key it touches; a write first clears every row that
45
+ * has already expired, so a key nobody reads again does not live forever.
46
+ */
47
+ private sweep;
43
48
  private live;
44
49
  get(key: string): Promise<string | null>;
45
50
  set(key: string, value: string, ttlSeconds?: number): Promise<void>;
46
51
  incr(key: string, ttlSeconds?: number): Promise<number>;
47
52
  del(key: string): Promise<void>;
53
+ /** Test helper. */
54
+ size(): number;
48
55
  close(): void;
49
56
  }
@@ -78,6 +78,15 @@ export class SqliteStore {
78
78
  this.database = new DatabaseSync(path);
79
79
  this.clock = clock;
80
80
  this.database.exec("CREATE TABLE IF NOT EXISTS proactive_gate_store (key TEXT PRIMARY KEY NOT NULL, value TEXT NOT NULL, expires_at INTEGER)");
81
+ // The partial index keeps a sweep proportional to the dead rows instead of a table scan.
82
+ this.database.exec("CREATE INDEX IF NOT EXISTS proactive_gate_store_expires_at ON proactive_gate_store (expires_at) WHERE expires_at IS NOT NULL");
83
+ }
84
+ /**
85
+ * A read prunes only the key it touches; a write first clears every row that
86
+ * has already expired, so a key nobody reads again does not live forever.
87
+ */
88
+ sweep(now) {
89
+ this.database.prepare("DELETE FROM proactive_gate_store WHERE expires_at IS NOT NULL AND expires_at <= ?").run(now);
81
90
  }
82
91
  live(key) {
83
92
  const row = this.database.prepare("SELECT value, expires_at FROM proactive_gate_store WHERE key = ?").get(key);
@@ -93,7 +102,9 @@ export class SqliteStore {
93
102
  return this.live(key)?.value ?? null;
94
103
  }
95
104
  async set(key, value, ttlSeconds) {
96
- const expiresAt = ttlSeconds ? this.clock() + ttlSeconds * 1000 : null;
105
+ const now = this.clock();
106
+ const expiresAt = ttlSeconds ? now + ttlSeconds * 1000 : null;
107
+ this.sweep(now);
97
108
  this.database
98
109
  .prepare("INSERT INTO proactive_gate_store (key, value, expires_at) VALUES (?, ?, ?) ON CONFLICT(key) DO UPDATE SET value = excluded.value, expires_at = excluded.expires_at")
99
110
  .run(key, value, expiresAt);
@@ -101,6 +112,7 @@ export class SqliteStore {
101
112
  async incr(key, ttlSeconds) {
102
113
  const now = this.clock();
103
114
  const expiresAt = ttlSeconds ? now + ttlSeconds * 1000 : null;
115
+ this.sweep(now);
104
116
  const row = this.database
105
117
  .prepare("INSERT INTO proactive_gate_store (key, value, expires_at) VALUES (?, '1', ?) ON CONFLICT(key) DO UPDATE SET value = CASE WHEN proactive_gate_store.expires_at IS NOT NULL AND proactive_gate_store.expires_at <= ? THEN '1' ELSE CAST(CAST(proactive_gate_store.value AS INTEGER) + 1 AS TEXT) END, expires_at = CASE WHEN proactive_gate_store.expires_at IS NOT NULL AND proactive_gate_store.expires_at <= ? THEN excluded.expires_at ELSE proactive_gate_store.expires_at END RETURNING value")
106
118
  .get(key, expiresAt, now, now);
@@ -109,6 +121,11 @@ export class SqliteStore {
109
121
  async del(key) {
110
122
  this.database.prepare("DELETE FROM proactive_gate_store WHERE key = ?").run(key);
111
123
  }
124
+ /** Test helper. */
125
+ size() {
126
+ const row = this.database.prepare("SELECT COUNT(*) AS n FROM proactive_gate_store").get();
127
+ return row.n;
128
+ }
112
129
  close() {
113
130
  this.database.close();
114
131
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "proactive-gate",
3
- "version": "0.3.0",
3
+ "version": "0.3.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",
@@ -48,7 +48,7 @@
48
48
  "sideEffects": false,
49
49
  "scripts": {
50
50
  "build": "tsc -p tsconfig.json",
51
- "test": "npm run build && node test/spec-lint.mjs && node --test dist/test/gate.test.js dist/test/conformance.test.js dist/test/presets.test.js dist/test/adapters.test.js dist/test/init.test.js dist/test/properties.test.js dist/test/dedupe.test.js test/release.test.mjs test/examples.test.mjs test/naive.test.mjs test/suite.test.mjs",
51
+ "test": "npm run build && node test/spec-lint.mjs && node --test dist/test/gate.test.js dist/test/conformance.test.js dist/test/presets.test.js dist/test/adapters.test.js dist/test/init.test.js dist/test/properties.test.js dist/test/dedupe.test.js test/release.test.mjs test/examples.test.mjs test/example-imports.test.mjs test/naive.test.mjs test/suite.test.mjs",
52
52
  "lint": "tsc -p tsconfig.json --noEmit",
53
53
  "spec-lint": "node test/spec-lint.mjs",
54
54
  "conformance": "npm run build && node dist/src/cli.js replay --fixtures spec/fixtures",
@@ -59,7 +59,8 @@
59
59
  "release-gate": "npm run build && node scripts/release-gate.mjs",
60
60
  "trace-svg": "npm run build && node scripts/trace-svg.mjs",
61
61
  "bench:compare": "npm run build && node bench/compare.mjs",
62
- "conformance-table": "node scripts/conformance-table.mjs"
62
+ "conformance-table": "node scripts/conformance-table.mjs",
63
+ "og": "npm run build && node scripts/og-image.mjs"
63
64
  },
64
65
  "engines": {
65
66
  "node": ">=20"