@alma-harness/postgres 0.1.0

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.
@@ -0,0 +1,440 @@
1
+ import { SessionStore, Scope, Msg, LoadOpts, ToolTrafficExpiry, EpisodeStore, CopySurface, EpisodeInput, Episode, EpisodeQuery, EpisodeQueryResult, ErasureSelector, TombstoneResult, ErasureWatermarkStore, ProfileStore, ProfileReadOpts, Profile, FactObservation, ObserveResult, InvalidateResult, SpendStore, SpendKey, SpendTotals, AuditLog, AccessEvent, RoutingEvent, CostEvent, RecallEvent, ContextEvent, TurnStore, LeaseOpts, TurnLease, TurnKey, TurnClaim, CompletedTurn } from '@alma-harness/core';
2
+ export { AuditLog, EpisodeStore, ProfileStore, SessionStore, SpendStore, TurnStore } from '@alma-harness/core';
3
+ import { Pool } from 'pg';
4
+
5
+ /**
6
+ * The RLS binding, shared by every Postgres store — spec 001, spec 011.
7
+ *
8
+ * One implementation, because "the scoped transaction" is exactly the place a
9
+ * copy-paste divergence would silently weaken tenancy: a store that forgot the
10
+ * `SET LOCAL ROLE`, or set the scope after the first query, would still pass
11
+ * its functional tests while running with isolation off.
12
+ */
13
+ interface ScopedStoreOptions {
14
+ /**
15
+ * Per-transaction `statement_timeout` in milliseconds (default 30s; `null`
16
+ * disables). A store whose queries are shaped by model-supplied input needs
17
+ * a ceiling that does not depend on the caller remembering one — the
18
+ * adversarial review drove a single search query to 25 seconds while it held
19
+ * a pooled connection. Set transaction-locally, so it resets on commit.
20
+ */
21
+ statementTimeoutMs?: number | null;
22
+ /**
23
+ * Role assumed per transaction via SET LOCAL ROLE, so RLS binds even when
24
+ * the connection user is privileged (superusers bypass RLS; dev and CI both
25
+ * connect as superusers — spec 001). `null` opts out, which weakens
26
+ * defense-in-depth to application-level isolation only.
27
+ */
28
+ role?: string | null;
29
+ }
30
+
31
+ type PostgresSessionStoreOptions = ScopedStoreOptions;
32
+ /**
33
+ * Reference `SessionStore` adapter (§6.6, §6.8) — spec 001.
34
+ *
35
+ * Transactional seq: `append` claims a seq range with an atomic counter
36
+ * upsert, so concurrent appenders serialize on the session row. Every
37
+ * operation runs inside a transaction scoped by transaction-local settings
38
+ * (`alma.org` / `alma.uid`) that the RLS policies compare against.
39
+ */
40
+ declare class PostgresSessionStore implements SessionStore {
41
+ #private;
42
+ constructor(pool: Pool, opts?: PostgresSessionStoreOptions);
43
+ append(scope: Scope, sessionId: string, entries: Msg[]): Promise<void>;
44
+ load(scope: Scope, sessionId: string, opts?: LoadOpts): Promise<Msg[]>;
45
+ expireToolTraffic(scope: Scope, sessionId: string, opts: {
46
+ inactiveSince: string;
47
+ }): Promise<ToolTrafficExpiry>;
48
+ erase(scope: Scope, sessionId?: string): Promise<void>;
49
+ }
50
+
51
+ /**
52
+ * Reference memory adapters (§6.7, §6.8) — spec 011. Same RLS discipline as
53
+ * the session store: every operation runs inside a transaction that assumes
54
+ * the app role and binds `alma.org`/`alma.uid`, which the policies compare
55
+ * against.
56
+ *
57
+ * Ranking is NOT done in SQL. The adapter filters CANDIDATES — a superset of
58
+ * what the tokenizer matches — and hands them to core's `rankEpisodes`, so
59
+ * every backend orders results identically (spec 011).
60
+ */
61
+ type PostgresMemoryStoreOptions = ScopedStoreOptions;
62
+ declare class PostgresEpisodeStore implements EpisodeStore {
63
+ #private;
64
+ readonly copySurfaces: readonly CopySurface[];
65
+ constructor(pool: Pool, opts?: PostgresMemoryStoreOptions);
66
+ append(scope: Scope, input: EpisodeInput): Promise<Episode>;
67
+ query(scope: Scope, q: EpisodeQuery): Promise<EpisodeQueryResult>;
68
+ get(scope: Scope, episodeIds: readonly string[]): Promise<Episode[]>;
69
+ tombstone(scope: Scope, selector: ErasureSelector, rawAt: string): Promise<TombstoneResult>;
70
+ archive(scope: Scope, episodeIds: readonly string[]): Promise<number>;
71
+ }
72
+ declare class PostgresProfileStore implements ProfileStore {
73
+ #private;
74
+ readonly copySurfaces: readonly CopySurface[];
75
+ constructor(pool: Pool, opts?: PostgresMemoryStoreOptions);
76
+ get(scope: Scope, opts?: ProfileReadOpts): Promise<Profile>;
77
+ observe(scope: Scope, obs: readonly FactObservation[]): Promise<readonly ObserveResult[]>;
78
+ setProtected(scope: Scope, facts: readonly FactObservation[]): Promise<readonly ObserveResult[]>;
79
+ invalidateBySource(scope: Scope, episodeIds: readonly string[] | "all", rawAt: string): Promise<InvalidateResult>;
80
+ }
81
+ /**
82
+ * The scope's last erasure timestamp — the in-flight guard slice 013 compares
83
+ * against, so a job that started before an erasure cannot submit afterwards
84
+ * and re-materialize erased content.
85
+ */
86
+ declare class PostgresErasureWatermarks implements ErasureWatermarkStore {
87
+ #private;
88
+ constructor(pool: Pool, opts?: PostgresMemoryStoreOptions);
89
+ get(scope: Scope): Promise<string | null>;
90
+ set(scope: Scope, rawAt: string): Promise<void>;
91
+ }
92
+
93
+ /**
94
+ * Schema and migration for the session store — spec 001 — plus the RLS
95
+ * building blocks every Alma table shares (spec 011).
96
+ *
97
+ * Scope-stamped tables under row-level security. Migrations are idempotent
98
+ * (safe to run on every startup) and creating/granting the RLS role is part of
99
+ * them: defense-in-depth must not depend on a manual step.
100
+ */
101
+ declare const DEFAULT_RLS_ROLE = "alma_app";
102
+ /**
103
+ * Role and table names are interpolated into DDL (Postgres cannot parameterize
104
+ * identifiers), so they are validated strictly — never raw.
105
+ */
106
+ declare function assertRoleIdentifier(role: string): void;
107
+ /**
108
+ * The scope-isolation policy, identical on every Alma table — spec 001, §6.8.
109
+ * One generator, because a table that received a hand-copied policy with a
110
+ * dropped `with check` would still pass its functional tests while accepting
111
+ * cross-tenant writes.
112
+ *
113
+ * `keys` exists for the one org-keyed table (the spend store's tenant-day
114
+ * counter, which has no uid column — spec: spend-store); every scope-stamped
115
+ * table takes the default. Parameterized rather than forked, so RLS
116
+ * hardening lands on every table at once (review finding).
117
+ */
118
+ declare function rlsPolicySql(table: string, keys?: readonly ("org" | "uid")[]): string;
119
+ /** Creates the RLS role if it does not exist, tolerating a concurrent race. */
120
+ declare function roleBootstrapSql(role: string): string;
121
+ /**
122
+ * DECISION (spec 001): SQL lives as a TS constant, not a .sql file — packages
123
+ * ship TypeScript source in Phase 0/1, and a migration runner would be
124
+ * premature for one migration.
125
+ */
126
+ declare function sessionStoreMigrationSql(role?: string): string;
127
+ /** Idempotent; running it is the product's choice (typically at startup). */
128
+ declare function migrateSessionStore(pool: Pool, opts?: {
129
+ role?: string;
130
+ }): Promise<void>;
131
+
132
+ /**
133
+ * Schema and migration for the memory stores — spec 011.
134
+ *
135
+ * Three scope-stamped tables under the same row-level-security discipline as
136
+ * the session store (spec 001): policies keyed on the transaction-local
137
+ * `alma.org`/`alma.uid` settings, `FORCE ROW LEVEL SECURITY`, and a dedicated
138
+ * non-superuser role assumed per transaction. The migration is idempotent.
139
+ */
140
+ declare const EPISODES_TABLE = "alma_memory_episodes";
141
+ declare const FACTS_TABLE = "alma_memory_facts";
142
+ declare const SCOPE_STATE_TABLE = "alma_memory_scope_state";
143
+ /**
144
+ * DECISION (spec 011): `importance` and `confidence` are `double precision`,
145
+ * not `real`. float4 round-trips through a text protocol with just enough
146
+ * digits to survive, and a 0.7 that comes back 0.699999988 would make the
147
+ * shared contract suite disagree with every other backend for no reason.
148
+ *
149
+ * DECISION: no pgvector column yet. Lexical ranking first (spec 010); the
150
+ * embedding column and its index arrive with the consumer that needs them,
151
+ * and will be declared as an additional COPY SURFACE so erasure reaches it.
152
+ */
153
+ declare function memoryStoreMigrationSql(role?: string): string;
154
+ /** Idempotent; running it is the product's choice (typically at startup). */
155
+ declare function migrateMemoryStores(pool: Pool, opts?: {
156
+ role?: string;
157
+ }): Promise<void>;
158
+
159
+ type PostgresSpendStoreOptions = ScopedStoreOptions;
160
+ /**
161
+ * Reference `SpendStore` adapter — spec: spend-store. Same RLS discipline as
162
+ * every other store; the tenant-day counter's policy is org-only, because the
163
+ * counter is (see `spend-schema.ts`).
164
+ *
165
+ * `add` is one transaction over two atomic upserts, each
166
+ * increment-and-RETURNING, so a concurrent add serializes on the row lock and
167
+ * every caller observes its own distinct running total — the property the
168
+ * budget guard's enforcement stands on. The session row is always touched
169
+ * before the day row, so two adds can never order the same pair of locks
170
+ * differently and deadlock.
171
+ *
172
+ * The UTC day bucket is derived in TS — the same `Date.parse` reading the
173
+ * in-memory reference and the memory tier's `toIsoInstant` use — and reaches
174
+ * SQL as a finished `date` literal. Deriving it server-side read the
175
+ * timestamp in the SERVER's TimeZone while the reference read it in the
176
+ * process's, so an offset-less stamp could credit different day counters in
177
+ * the two stores (review finding — the same bug the episode tier fixed once
178
+ * already).
179
+ */
180
+ declare class PostgresSpendStore implements SpendStore {
181
+ #private;
182
+ constructor(pool: Pool, opts?: PostgresSpendStoreOptions);
183
+ add(entry: SpendKey & {
184
+ usd: number;
185
+ }): Promise<SpendTotals>;
186
+ peek(key: SpendKey): Promise<SpendTotals>;
187
+ }
188
+
189
+ type PostgresAuditLogOptions = ScopedStoreOptions;
190
+ /**
191
+ * Reference `AuditLog` sink — §6.8, spec 038. Same RLS discipline as every
192
+ * other store in this package: each write runs inside a transaction that
193
+ * assumes the app role and binds `alma.org`/`alma.uid` for the policies to
194
+ * compare against.
195
+ *
196
+ * WRITES SYNCHRONOUSLY, and ships no buffering wrapper. The contract permits
197
+ * either — "buffer internally and return synchronously to stay off the critical
198
+ * path, or return a promise and be awaited" — and the cost of this choice is
199
+ * real: a three-step turn with two tool calls emits one routing, three cost and
200
+ * two access events, plus recall and context, so roughly eight round trips join
201
+ * the turn's critical path.
202
+ *
203
+ * The alternative is worse in the way that matters. A buffered sink that loses
204
+ * its buffer on a crash stops writing SILENTLY, which is exactly the failure
205
+ * `AuditSinkError` was typed to catch (spec 027) and exactly what "where trails
206
+ * are written is swappable; THAT they are written is not" forbids. A product
207
+ * that wants the trade should own a visible wrapper, not inherit it from the
208
+ * reference adapter.
209
+ *
210
+ * There is no read surface here, deliberately (spec 038). §7.3's thesis is that
211
+ * "what exactly did the model see about this user?" is a query — but which
212
+ * queries matter is not yet known, and a contract shaped before its consumers
213
+ * exist is public API from its first release. The product queries its own
214
+ * tables; the rule of two decides when one has earned promotion.
215
+ */
216
+ declare class PostgresAuditLog implements AuditLog {
217
+ #private;
218
+ constructor(pool: Pool, opts?: PostgresAuditLogOptions);
219
+ access(e: AccessEvent): Promise<void>;
220
+ routing(e: RoutingEvent): Promise<void>;
221
+ cost(e: CostEvent): Promise<void>;
222
+ recall(e: RecallEvent): Promise<void>;
223
+ context(e: ContextEvent): Promise<void>;
224
+ }
225
+
226
+ /**
227
+ * Schema and migration for the audit trails — spec 038.
228
+ *
229
+ * FIVE tables, one per event family, rather than one with a `payload jsonb`.
230
+ * The shapes are genuinely different, but the deciding argument is that this
231
+ * makes the metadata-only guarantee STRUCTURAL: `audit.ts` promises trails
232
+ * carry "METADATA ONLY, never content — by construction, not by reviewer
233
+ * vigilance", and `AccessEvent.resource` is documented as an identifier and
234
+ * never its content. As a `text` column named `resource` that is a constraint
235
+ * a reviewer can look at and a DBA can audit; as a key inside a bag, anything
236
+ * fits and nothing notices.
237
+ *
238
+ * It also leaves room for what comes next: cost feeds billing and must be
239
+ * kept, while recall and context are diagnostic and can expire early. Separate
240
+ * tables make that a policy per table rather than a `where family = …` smeared
241
+ * across every statement.
242
+ */
243
+ declare const AUDIT_ACCESS_TABLE = "alma_audit_access";
244
+ declare const AUDIT_ROUTING_TABLE = "alma_audit_routing";
245
+ declare const AUDIT_COST_TABLE = "alma_audit_cost";
246
+ declare const AUDIT_RECALL_TABLE = "alma_audit_recall";
247
+ declare const AUDIT_CONTEXT_TABLE = "alma_audit_context";
248
+ /**
249
+ * Role the retention sweep assumes — spec 039 review.
250
+ *
251
+ * It exists because the app role deliberately cannot delete (spec 038) and the
252
+ * sweep therefore cannot run as it. Created and granted by the migration, for
253
+ * the reason `roleBootstrapSql` exists at all: defence in depth must not depend
254
+ * on a manual step (spec 001).
255
+ */
256
+ declare const DEFAULT_RETENTION_ROLE = "alma_retention";
257
+ declare const AUDIT_TABLES: readonly ["alma_audit_access", "alma_audit_routing", "alma_audit_cost", "alma_audit_recall", "alma_audit_context"];
258
+ /**
259
+ * DECISION (spec 038): the primary key is a `uuid` defaulted by
260
+ * `gen_random_uuid()`, not a `bigserial`. A trail is append-only with no
261
+ * natural key, and a sequence would need its own `usage` grant for the
262
+ * non-superuser app role — one more thing to forget in a migration whose whole
263
+ * point is that defense-in-depth must not depend on a manual step (spec 001).
264
+ * `gen_random_uuid()` is core Postgres since 13; no extension.
265
+ *
266
+ * DECISION (spec 038): the grant carries `select, insert` — no `delete`, and
267
+ * no `update`. The spend counters' posture, for the same reason: these are
268
+ * content-free records kept as evidence, and an erasure that removed the proof
269
+ * an erasure happened is not an improvement (§10). `update` is excluded too,
270
+ * because an audit row that can be edited is not an audit row.
271
+ *
272
+ * DECISION (spec 038): optional array fields (`capsCrossed`, `degradedTiers`,
273
+ * `refused`) are `text[] not null default '{}'`. Absent and empty mean the
274
+ * same thing for all three — no caps crossed, no tiers degraded, nothing
275
+ * refused — so normalising removes a null check rather than losing a
276
+ * distinction.
277
+ */
278
+ declare function auditLogMigrationSql(role?: string, retentionRole?: string): string;
279
+ /** Idempotent; running it is the product's choice (typically at startup). */
280
+ declare function migrateAuditLog(pool: Pool, opts?: {
281
+ role?: string;
282
+ retentionRole?: string;
283
+ }): Promise<void>;
284
+ /** The audit table names, as a type — one window per family (spec 039). */
285
+ type AuditTable = (typeof AUDIT_TABLES)[number];
286
+ /**
287
+ * Deletes audit rows older than each family's cutoff — spec 039.
288
+ *
289
+ * This resolves a consequence spec 038 created without naming: the app role
290
+ * was granted `select, insert` and NOT `delete`, so a scoped erasure cannot
291
+ * remove the proof an erasure happened — which also means the app role cannot
292
+ * expire audit rows.
293
+ *
294
+ * DECISION (spec 039): erasure may not delete audit rows; time-based retention
295
+ * may, and they are DIFFERENT ACTORS. A scoped purge runs as the app role and
296
+ * is blocked at the grant. Retention is maintenance: it runs as the pool's own
297
+ * role, crosses scopes by nature, and deliberately does NOT go through the
298
+ * RLS-bound `inScope` path.
299
+ *
300
+ * A plain function taking the pool rather than a method on a store, and the
301
+ * shape is the point — a method on a seam would suggest it participates in the
302
+ * scope-bound discipline, and this does not. Passing the pool makes the
303
+ * privilege visible at the call site.
304
+ *
305
+ * PER FAMILY, because that is what five tables bought: cost feeds billing and
306
+ * is kept for years while recall and context are diagnostic and can go in
307
+ * weeks. A single window would have made the split pointless. A table absent
308
+ * from `windows` is not touched.
309
+ *
310
+ * It ASSUMES {@link DEFAULT_RETENTION_ROLE} for the duration of one
311
+ * transaction, and that is the correction the spec 039 review forced. The first
312
+ * version ran the deletes straight on the pool, on the theory that it therefore
313
+ * "bypassed RLS". It did not: every audit table carries FORCE ROW LEVEL
314
+ * SECURITY, which subjects even the table owner to the predicate, so the sweep
315
+ * matched the scope-keyed policy with no scope bound, deleted ZERO rows, and
316
+ * reported success. It appeared to work only because dev and CI connect as
317
+ * superusers — the same elevation `scoped.ts` calls out as the reason every
318
+ * other store must actively assume a role rather than assume privilege.
319
+ *
320
+ * A retention mechanism that silently retains forever, in exactly the
321
+ * deployments careful enough not to connect as a superuser, is the failure this
322
+ * whole slice exists to prevent one layer up.
323
+ */
324
+ declare function purgeAuditBefore(pool: Pool, windows: Partial<Record<AuditTable, string>>, opts?: {
325
+ retentionRole?: string;
326
+ }): Promise<Partial<Record<AuditTable, number>>>;
327
+
328
+ type PostgresTurnStoreOptions = ScopedStoreOptions;
329
+ /**
330
+ * Reference `TurnStore` adapter — spec 030. Same RLS discipline as every other
331
+ * store in this package.
332
+ *
333
+ * The lease is ONE conditional upsert: the row is taken only when there is no
334
+ * row or the existing one has expired, and Postgres serializes contenders on
335
+ * the primary key, so exactly one concurrent caller sees a returned row. That
336
+ * single statement is the whole mutual-exclusion argument — a read-then-write
337
+ * would let two callers both observe a free session.
338
+ *
339
+ * Waiting is POLLING with backoff, not `LISTEN`/`NOTIFY` or an advisory lock.
340
+ * An advisory lock is bound to the connection that took it, and a turn holds
341
+ * its lease across many queries from a POOL — the lease has to outlive any one
342
+ * connection, which is what makes it a row.
343
+ */
344
+ declare class PostgresTurnStore implements TurnStore {
345
+ #private;
346
+ constructor(pool: Pool, opts?: PostgresTurnStoreOptions);
347
+ acquire(scope: Scope, sessionId: string, opts: LeaseOpts): Promise<TurnLease | null>;
348
+ release(scope: Scope, sessionId: string, lease: TurnLease): Promise<void>;
349
+ claim(key: TurnKey): Promise<TurnClaim>;
350
+ /**
351
+ * PRECONDITION: every string in `completed`, keys included, is well-formed
352
+ * UTF-16 — `jsonb` refuses a lone surrogate and the write fails (spec:
353
+ * well-formed-text).
354
+ *
355
+ * The loop repairs it on the way in: `reply` is drawn from messages
356
+ * `record()` passed through `toWellFormedDeep`. But that repair is
357
+ * BEST-EFFORT by design — its `catch` keeps the unrepaired message, because
358
+ * failing to repair must never cost more than not having tried — so a
359
+ * pathologically nested payload can still arrive malformed.
360
+ *
361
+ * Now ENFORCED here and in the in-memory reference alike, by the same guard
362
+ * `SessionStore.append` uses (spec 040). The adapters used to differ — that
363
+ * store kept the lone surrogate, this one refused the write — which is the
364
+ * gap spec 025's review recorded and two contracts then documented instead
365
+ * of closing. The check runs BEFORE the UPDATE and regardless of whether a
366
+ * row matches, because `$5::jsonb` is parsed either way; the in-memory
367
+ * reference orders it the same for that reason. The first version of this
368
+ * comment claimed the loop simply guaranteed it (spec 033).
369
+ */
370
+ complete(key: TurnKey, completed: CompletedTurn): Promise<void>;
371
+ abandon(key: TurnKey): Promise<void>;
372
+ erase(scope: Scope, sessionId?: string): Promise<void>;
373
+ }
374
+
375
+ /**
376
+ * Schema and migration for the turn store — spec 030.
377
+ *
378
+ * Two scope-stamped tables under the standard `{org, uid}` policy, through the
379
+ * SAME shared generator every other Alma table uses: forking it would let
380
+ * future RLS hardening skip these two (the finding spec: spend-store recorded).
381
+ *
382
+ * The lease is one row per session, replaced in place; the claim is one row
383
+ * per idempotency key, holding the replayable turn as `jsonb`.
384
+ */
385
+ declare const TURN_LEASES_TABLE = "alma_turn_leases";
386
+ declare const TURN_CLAIMS_TABLE = "alma_turn_claims";
387
+ /**
388
+ * DECISION (spec 030): `expires_at` is a `timestamptz` computed from the
389
+ * SERVER's `now()`, not from the process clock. Every contender for one lease
390
+ * must compare against one clock, and two app instances with a few seconds of
391
+ * drift would otherwise disagree about whether a lease is live — which is
392
+ * exactly the disagreement that hands the same session to two holders. This is
393
+ * the opposite of the spend store's day bucket, which is derived in TS
394
+ * precisely because it must match the reference implementation's reading of a
395
+ * caller-supplied timestamp; here there is no caller timestamp to match.
396
+ *
397
+ * DECISION (spec 030): `completed` is nullable, and NULL means in flight. The
398
+ * claim row is inserted before the turn runs and updated when it finishes, so
399
+ * the row's existence is the claim and its content is the result.
400
+ *
401
+ * The grant carries `delete`: unlike spend counters, these rows hold content
402
+ * (the reply verbatim) and §10 erasure must be able to remove them.
403
+ */
404
+ declare function turnStoreMigrationSql(role?: string): string;
405
+ /** Idempotent; running it is the product's choice (typically at startup). */
406
+ declare function migrateTurnStore(pool: Pool, opts?: {
407
+ role?: string;
408
+ }): Promise<void>;
409
+
410
+ /**
411
+ * Schema and migration for the spend store — spec: spend-store.
412
+ *
413
+ * Two counter tables. The session counter is scope-stamped and carries the
414
+ * standard `{org, uid}` policy. The tenant-day counter is deliberately
415
+ * ORG-KEYED — it aggregates across every uid and session of the org, which is
416
+ * what an operator caps or watches — so its policy compares org alone, via
417
+ * the SAME shared generator (`rlsPolicySql(table, ["org"])`): forking the
418
+ * generator would let future RLS hardening skip this one table (review
419
+ * finding).
420
+ */
421
+ declare const SPEND_SESSIONS_TABLE = "alma_spend_sessions";
422
+ declare const SPEND_TENANT_DAYS_TABLE = "alma_spend_tenant_days";
423
+ /**
424
+ * DECISION (spec: spend-store): `usd` is `double precision`, like every
425
+ * fractional number in this package — the whole pricing pipeline computes in
426
+ * JS floats, and a NUMERIC column would round-trip as a string the driver
427
+ * does not sum. A single step costs fractions of a cent; the contract suite
428
+ * pins that nothing rounds it away.
429
+ *
430
+ * The grant carries NO delete: spend counters are retained through scoped
431
+ * purge (a financial record, not personal content — spec: spend-store), and
432
+ * the app role simply cannot remove one.
433
+ */
434
+ declare function spendStoreMigrationSql(role?: string): string;
435
+ /** Idempotent; running it is the product's choice (typically at startup). */
436
+ declare function migrateSpendStore(pool: Pool, opts?: {
437
+ role?: string;
438
+ }): Promise<void>;
439
+
440
+ export { AUDIT_ACCESS_TABLE, AUDIT_CONTEXT_TABLE, AUDIT_COST_TABLE, AUDIT_RECALL_TABLE, AUDIT_ROUTING_TABLE, AUDIT_TABLES, type AuditTable, DEFAULT_RETENTION_ROLE, DEFAULT_RLS_ROLE, EPISODES_TABLE, FACTS_TABLE, PostgresAuditLog, type PostgresAuditLogOptions, PostgresEpisodeStore, PostgresErasureWatermarks, type PostgresMemoryStoreOptions, PostgresProfileStore, PostgresSessionStore, type PostgresSessionStoreOptions, PostgresSpendStore, type PostgresSpendStoreOptions, PostgresTurnStore, type PostgresTurnStoreOptions, SCOPE_STATE_TABLE, SPEND_SESSIONS_TABLE, SPEND_TENANT_DAYS_TABLE, type ScopedStoreOptions, TURN_CLAIMS_TABLE, TURN_LEASES_TABLE, assertRoleIdentifier, auditLogMigrationSql, memoryStoreMigrationSql, migrateAuditLog, migrateMemoryStores, migrateSessionStore, migrateSpendStore, migrateTurnStore, purgeAuditBefore, rlsPolicySql, roleBootstrapSql, sessionStoreMigrationSql, spendStoreMigrationSql, turnStoreMigrationSql };