proactive-gate 0.2.5 → 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 +43 -5
- package/README.tr.md +22 -2
- package/dist/src/store-contract.d.ts +11 -0
- package/dist/src/store-contract.js +118 -0
- package/dist/src/stores.d.ts +7 -0
- package/dist/src/stores.js +18 -1
- package/package.json +9 -4
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,29 @@ 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
|
-
[@
|
|
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).
|
|
447
|
+
|
|
448
|
+
**Writing your own store?** `proactive-gate/store-contract` exports the same suite these three are
|
|
449
|
+
held to, so you can prove yours behaves rather than hope:
|
|
450
|
+
|
|
451
|
+
```ts
|
|
452
|
+
import { storeContract } from "proactive-gate/store-contract";
|
|
453
|
+
storeContract("PostgresStore", (clock) => new PostgresStore({ clock }));
|
|
454
|
+
```
|
|
455
|
+
|
|
456
|
+
It checks `get`, `set` and `del`, `incr` from an absent key, concurrent `incr` atomicity, the
|
|
457
|
+
expiry boundary, and that a TTL given to `set` and one given to `incr` agree, then replays a
|
|
458
|
+
seeded random operation sequence against `MemoryStore`. A store whose backend owns the clock
|
|
459
|
+
passes `expiry: "skip"` and those cases are reported as skipped rather than quietly dropped. It
|
|
460
|
+
was contributed by [@Aaqibhafeezkhan](https://github.com/Aaqibhafeezkhan) in
|
|
461
|
+
[#24](https://github.com/Bubblegunn/proactive-gate/pull/24), and lives on its own subpath so
|
|
462
|
+
importing the package never pulls `node:test` into your bundle. See
|
|
463
|
+
[docs/store-contract.md](docs/store-contract.md).
|
|
438
464
|
|
|
439
465
|
## Fail open, on purpose
|
|
440
466
|
|
|
@@ -546,6 +572,15 @@ candidates with the clock taken from each line, and `node examples/ai-sdk/run.mj
|
|
|
546
572
|
day of tool-approval requests, one of which is a critical alert that a legal window (the TCPA
|
|
547
573
|
preset) still refuses. Both are part of `npm run examples` and of the test suite.
|
|
548
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
|
+
|
|
549
584
|
## Python
|
|
550
585
|
|
|
551
586
|
```
|
|
@@ -720,11 +755,14 @@ trace, and adds the part they leave out: the budget consumed at send time.
|
|
|
720
755
|
## Thanks
|
|
721
756
|
|
|
722
757
|
Two people sent pull requests on the day this was published, neither of whom I had spoken to
|
|
723
|
-
before. [@
|
|
758
|
+
before. [@Aaqibhafeezkhan](https://github.com/Aaqibhafeezkhan) wrote `SqliteStore`
|
|
724
759
|
([#3](https://github.com/Bubblegunn/proactive-gate/pull/3)) and
|
|
725
760
|
[@edwardsong08](https://github.com/edwardsong08) wrote the weekly budget
|
|
726
761
|
([#9](https://github.com/Bubblegunn/proactive-gate/pull/9)). Both shipped in 0.1.2 and are in
|
|
727
|
-
every release since, including the one you install today.
|
|
762
|
+
every release since, including the one you install today. @Aaqibhafeezkhan came back for a
|
|
763
|
+
second one and wrote the store contract suite in [#24](https://github.com/Bubblegunn/proactive-gate/pull/24).
|
|
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).
|
|
728
766
|
|
|
729
767
|
## Cite this
|
|
730
768
|
|
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"`.
|
|
341
|
-
|
|
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
|
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
import type { Store } from "./types.js";
|
|
2
|
+
export interface StoreContractHandle {
|
|
3
|
+
store: Store;
|
|
4
|
+
teardown?: () => void | Promise<void>;
|
|
5
|
+
}
|
|
6
|
+
export type StoreContractFactory = (clock?: () => number) => Store | StoreContractHandle | Promise<Store | StoreContractHandle>;
|
|
7
|
+
export interface StoreContractOptions {
|
|
8
|
+
expiry?: "injected" | "skip";
|
|
9
|
+
skip?: string;
|
|
10
|
+
}
|
|
11
|
+
export declare function storeContract(name: string, factory: StoreContractFactory, options?: StoreContractOptions): void;
|
|
@@ -0,0 +1,118 @@
|
|
|
1
|
+
import { test } from "node:test";
|
|
2
|
+
import assert from "node:assert/strict";
|
|
3
|
+
import { MemoryStore } from "./stores.js";
|
|
4
|
+
function asHandle(value) {
|
|
5
|
+
return "store" in value ? value : { store: value };
|
|
6
|
+
}
|
|
7
|
+
export function storeContract(name, factory, options = {}) {
|
|
8
|
+
const expiry = options.expiry ?? "injected";
|
|
9
|
+
const skip = options.skip;
|
|
10
|
+
const testOptions = skip ? { skip } : {};
|
|
11
|
+
const expiryOptions = expiry === "skip" ? { skip: "expiry cases skipped: this store does not accept an injected clock" } : testOptions;
|
|
12
|
+
const withStore = async (clock, run) => {
|
|
13
|
+
const handle = asHandle(await factory(clock));
|
|
14
|
+
try {
|
|
15
|
+
return await run(handle.store);
|
|
16
|
+
}
|
|
17
|
+
finally {
|
|
18
|
+
await handle.teardown?.();
|
|
19
|
+
}
|
|
20
|
+
};
|
|
21
|
+
test(`${name}: get, set and del`, testOptions, async () => {
|
|
22
|
+
await withStore(undefined, async (store) => {
|
|
23
|
+
assert.equal(await store.get("missing"), null);
|
|
24
|
+
await store.set("key", "value");
|
|
25
|
+
assert.equal(await store.get("key"), "value");
|
|
26
|
+
await store.del("key");
|
|
27
|
+
assert.equal(await store.get("key"), null);
|
|
28
|
+
});
|
|
29
|
+
});
|
|
30
|
+
test(`${name}: incr from absent starts at one`, testOptions, async () => {
|
|
31
|
+
await withStore(undefined, async (store) => {
|
|
32
|
+
assert.equal(await store.incr("counter"), 1);
|
|
33
|
+
assert.equal(await store.incr("counter"), 2);
|
|
34
|
+
});
|
|
35
|
+
});
|
|
36
|
+
test(`${name}: incr is atomic`, testOptions, async () => {
|
|
37
|
+
await withStore(undefined, async (store) => {
|
|
38
|
+
const results = await Promise.all(Array.from({ length: 100 }, () => store.incr("counter")));
|
|
39
|
+
assert.deepEqual([...results].sort((a, b) => a - b), Array.from({ length: 100 }, (_, i) => i + 1));
|
|
40
|
+
assert.equal(await store.get("counter"), "100");
|
|
41
|
+
});
|
|
42
|
+
});
|
|
43
|
+
test(`${name}: set and incr TTLs expire at the same boundary`, expiryOptions, async () => {
|
|
44
|
+
let now = 0;
|
|
45
|
+
const clock = () => now;
|
|
46
|
+
await withStore(clock, async (store) => {
|
|
47
|
+
await store.set("set", "value", 2);
|
|
48
|
+
await store.incr("incr", 2);
|
|
49
|
+
now = 1999;
|
|
50
|
+
assert.equal(await store.get("set"), "value");
|
|
51
|
+
assert.equal(await store.get("incr"), "1");
|
|
52
|
+
now = 2000;
|
|
53
|
+
assert.equal(await store.get("set"), null);
|
|
54
|
+
assert.equal(await store.get("incr"), null);
|
|
55
|
+
});
|
|
56
|
+
});
|
|
57
|
+
test(`${name}: expiry is inclusive at the boundary`, expiryOptions, async () => {
|
|
58
|
+
let now = 1000;
|
|
59
|
+
const clock = () => now;
|
|
60
|
+
await withStore(clock, async (store) => {
|
|
61
|
+
await store.set("key", "value", 1);
|
|
62
|
+
assert.equal(await store.get("key"), "value");
|
|
63
|
+
now = 1999;
|
|
64
|
+
assert.equal(await store.get("key"), "value");
|
|
65
|
+
now = 2000;
|
|
66
|
+
assert.equal(await store.get("key"), null);
|
|
67
|
+
});
|
|
68
|
+
});
|
|
69
|
+
test(`${name}: random operations match MemoryStore`, testOptions, async () => {
|
|
70
|
+
for (let seed = 1; seed <= 40; seed++) {
|
|
71
|
+
let now = 0;
|
|
72
|
+
const clock = () => now;
|
|
73
|
+
await withStore(expiry === "injected" ? clock : undefined, async (store) => {
|
|
74
|
+
const reference = new MemoryStore(clock);
|
|
75
|
+
const keys = ["a", "b", "c"];
|
|
76
|
+
let state = 0x6d2b79f5 ^ seed;
|
|
77
|
+
const random = () => {
|
|
78
|
+
state = (Math.imul(state ^ (state >>> 16), 2246822507) + 3266489909) >>> 0;
|
|
79
|
+
return state / 4294967296;
|
|
80
|
+
};
|
|
81
|
+
try {
|
|
82
|
+
for (let step = 0; step < 40; step++) {
|
|
83
|
+
const key = keys[Math.floor(random() * keys.length)];
|
|
84
|
+
const ttl = expiry === "injected" && random() > 0.5 ? 1 + Math.floor(random() * 3) : undefined;
|
|
85
|
+
const op = Math.floor(random() * 5);
|
|
86
|
+
if (op === 0) {
|
|
87
|
+
const value = String(Math.floor(random() * 100));
|
|
88
|
+
await reference.set(key, value, ttl);
|
|
89
|
+
await store.set(key, value, ttl);
|
|
90
|
+
}
|
|
91
|
+
else if (op === 1) {
|
|
92
|
+
assert.equal(await store.incr(key, ttl), await reference.incr(key, ttl), `seed ${seed} step ${step}: incr disagreed`);
|
|
93
|
+
}
|
|
94
|
+
else if (op === 2) {
|
|
95
|
+
await reference.del(key);
|
|
96
|
+
await store.del(key);
|
|
97
|
+
}
|
|
98
|
+
else if (op === 3) {
|
|
99
|
+
if (expiry === "injected")
|
|
100
|
+
now += Math.floor(random() * 3000);
|
|
101
|
+
}
|
|
102
|
+
else {
|
|
103
|
+
assert.equal(await store.get(key), await reference.get(key), `seed ${seed} step ${step}: get disagreed on ${key}`);
|
|
104
|
+
}
|
|
105
|
+
}
|
|
106
|
+
for (const key of keys) {
|
|
107
|
+
assert.equal(await store.get(key), await reference.get(key), `seed ${seed}: final state disagreed on ${key}`);
|
|
108
|
+
}
|
|
109
|
+
}
|
|
110
|
+
finally {
|
|
111
|
+
await reference.del("a");
|
|
112
|
+
await reference.del("b");
|
|
113
|
+
await reference.del("c");
|
|
114
|
+
}
|
|
115
|
+
});
|
|
116
|
+
}
|
|
117
|
+
});
|
|
118
|
+
}
|
package/dist/src/stores.d.ts
CHANGED
|
@@ -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
|
}
|
package/dist/src/stores.js
CHANGED
|
@@ -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
|
|
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
|
+
"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",
|
|
@@ -14,6 +14,10 @@
|
|
|
14
14
|
"types": "./dist/src/presets.d.ts",
|
|
15
15
|
"import": "./dist/src/presets.js"
|
|
16
16
|
},
|
|
17
|
+
"./store-contract": {
|
|
18
|
+
"types": "./dist/src/store-contract.d.ts",
|
|
19
|
+
"import": "./dist/src/store-contract.js"
|
|
20
|
+
},
|
|
17
21
|
"./ai-sdk": {
|
|
18
22
|
"types": "./dist/src/adapters/ai-sdk.d.ts",
|
|
19
23
|
"import": "./dist/src/adapters/ai-sdk.js"
|
|
@@ -44,7 +48,7 @@
|
|
|
44
48
|
"sideEffects": false,
|
|
45
49
|
"scripts": {
|
|
46
50
|
"build": "tsc -p tsconfig.json",
|
|
47
|
-
"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",
|
|
48
52
|
"lint": "tsc -p tsconfig.json --noEmit",
|
|
49
53
|
"spec-lint": "node test/spec-lint.mjs",
|
|
50
54
|
"conformance": "npm run build && node dist/src/cli.js replay --fixtures spec/fixtures",
|
|
@@ -55,7 +59,8 @@
|
|
|
55
59
|
"release-gate": "npm run build && node scripts/release-gate.mjs",
|
|
56
60
|
"trace-svg": "npm run build && node scripts/trace-svg.mjs",
|
|
57
61
|
"bench:compare": "npm run build && node bench/compare.mjs",
|
|
58
|
-
"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"
|
|
59
64
|
},
|
|
60
65
|
"engines": {
|
|
61
66
|
"node": ">=20"
|
|
@@ -65,9 +70,9 @@
|
|
|
65
70
|
"agents",
|
|
66
71
|
"proactive",
|
|
67
72
|
"notifications",
|
|
68
|
-
"rate-limit",
|
|
69
73
|
"quiet-hours",
|
|
70
74
|
"consent",
|
|
75
|
+
"rate-limit",
|
|
71
76
|
"budget",
|
|
72
77
|
"llm",
|
|
73
78
|
"policy",
|