/** * Postgres-backed GuardFireStatsStore. Plugs into core's `GuardFireStats` * accumulator so guard-fire counters survive restarts. * * Writes are UPSERTs that add to the per-day natural key. The natural key is * `(guard_name, guard_phase, decision_kind, day, pack_id)` and IS the * migration-006 PRIMARY KEY arbiter the additive `ON CONFLICT` targets. * * 052 — `pack_id` is `NOT NULL DEFAULT ''` (migration 006): the default * (no-pack) accumulator writes the empty-string sentinel, NOT NULL. A NULL * `pack_id` would (a) violate the PK column's implicit NOT NULL (Postgres 23502) * and (b) — since Postgres treats NULL as DISTINCT in PK/unique arbiters — * prevent the `ON CONFLICT` from ever matching two no-pack rows, so the upsert * would INSERT duplicates instead of aggregating (the over-count failure). The * empty-string sentinel makes the arbiter deterministic and the additive upsert * atomic/coalescing for every write, with or without a pack. * * Reads return rows newer-than the supplied ISO `since`, optionally * filtered by pack. */ import type { GuardFireBucket, GuardFireStatsStore, GuardPhase } from "@adjudicate/core"; import type { PostgresReader } from "./pg-reader.js"; /** * Adopter-implemented writer for guard-stats UPSERTs. Companion to * `PostgresGovernanceWriter` — kept distinct so adopters who only want * reads don't pay for the write dep. */ export interface GuardStatsWriter { upsertGuardStat(args: { readonly guardName: string; readonly guardPhase: GuardPhase; readonly decisionKind: GuardFireBucket["decisionKind"]; readonly day: string; readonly packId: string | null; readonly countDelta: number; }): Promise; } /** * Identifies one (resource, horizon) reservation row. The 5 fields are the * migration-006 PRIMARY KEY (the `ON CONFLICT` arbiter); `cap` is the inclusive * cumulative/velocity limit the reservation may not cross. * * The natural key reuses the guard-stats columns: a reservation against an * account-daily cap is, e.g., `{ guardName: "acct_7", guardPhase: "business", * decisionKind: "EXECUTE", day: "2026-06-19", packId: "pix" }`. Adopters choose * the encoding; the store treats them as opaque key parts. */ export interface ReservationKey { readonly guardName: string; readonly guardPhase: GuardPhase; readonly decisionKind: GuardFireBucket["decisionKind"]; readonly day: string; /** * 052 — `''` is the no-pack PK sentinel (NOT NULL). A NULL would violate the * PK column NOT NULL (23502) or — treated as DISTINCT — split the arbiter so * the additive upsert duplicates rows. The store coerces a null/empty packId * to `''` so the reservation arbiter matches deterministically. */ readonly packId: string | null; /** Inclusive cap for this horizon. A claim that would push the running * reserved total over `cap` is REFUSED (fail-closed, single statement). */ readonly cap: number; } /** * Adopter-implemented atomic reservation writer. Runs `RESERVE_GUARD_STAT_SQL` * (or its equivalent) and returns the affected row count: `1` when the `delta` * units were reserved, `0` when the claim would cross the cap (refused). The * caller MUST surface a non-positive count as a refusal — the over-commit guard * is the `rowCount === 0` signal, not an exception. */ export interface ReservationWriter { reserveGuardStat(args: { readonly guardName: string; readonly guardPhase: GuardPhase; readonly decisionKind: GuardFireBucket["decisionKind"]; readonly day: string; readonly packId: string; readonly delta: number; readonly cap: number; }): Promise; } /** * Outcome of a reservation claim. `reserved: true` ⇒ the `delta` units were * committed atomically (the row's running total stays ≤ cap). `reserved: false` * ⇒ the claim was REFUSED because it would cross the cap (or `delta` was * non-positive / `delta > cap`) — the durable over-commit guard fired. */ export type ReservationOutcome = { readonly reserved: true; } | { readonly reserved: false; readonly reason: "over_cap" | "invalid_delta"; }; export interface CreatePostgresReservationStoreDeps { readonly writer: ReservationWriter; } /** * Durable, transactional reservation store. EXTENDS the additive guard-stats * upsert template with an over-commit guard so a cumulative/velocity cap can be * decremented (claimed) under concurrency WITHOUT over-commit. * * `reserve(key, delta)` runs ONE atomic statement (`RESERVE_GUARD_STAT_SQL`): * - `delta <= 0` is rejected locally (`invalid_delta`) — a non-positive claim * would fabricate headroom (§C: never decrease friction); it never reaches * the DB. * - otherwise the writer's affected-row count is the verdict: `1` ⇒ reserved, * `0` ⇒ over-cap refusal. There is NO read-modify-write window — concurrent * over-cap claims cannot both win. * * IMPURE-SHELL ONLY (§D): this is store IO that happens AFTER the pure kernel * decision. It never enters `adjudicate()`; a refused claim is rolled back * through the existing rate-limit rollback closure in the kernel shell, and a * store/IO error on the write path aborts EXECUTE (it propagates — the caller's * await rejects — rather than failing open). */ export declare function createPostgresReservationStore(deps: CreatePostgresReservationStoreDeps): { reserve(key: ReservationKey, delta: number): Promise; }; export declare const UPSERT_GUARD_STAT_SQL = "\nINSERT INTO audit_guard_stats\n (guard_name, guard_phase, decision_kind, day, pack_id, count)\nVALUES ($1, $2, $3, $4, $5, $6)\nON CONFLICT (guard_name, guard_phase, decision_kind, day, pack_id)\nDO UPDATE SET count = audit_guard_stats.count + EXCLUDED.count\n"; export declare const RESERVE_GUARD_STAT_SQL = "\nINSERT INTO audit_guard_stats\n (guard_name, guard_phase, decision_kind, day, pack_id, count)\nSELECT $1, $2, $3, $4, $5, $6::bigint WHERE $6::bigint <= $7::bigint\nON CONFLICT (guard_name, guard_phase, decision_kind, day, pack_id)\nDO UPDATE SET count = audit_guard_stats.count + EXCLUDED.count\nWHERE audit_guard_stats.count + EXCLUDED.count <= $7::bigint\n"; export interface CreatePostgresGuardFireStatsStoreDeps { readonly reader: PostgresReader; readonly writer: GuardStatsWriter; } export declare function createPostgresGuardFireStatsStore(deps: CreatePostgresGuardFireStatsStoreDeps): GuardFireStatsStore; //# sourceMappingURL=guard-stats-store.d.ts.map