/**
 * StoreSnapshot — single-origin snapshot-then-tail machinery (WS2 replicated-store
 * foundation, Component 4 / build-order step 3).
 *
 * Spec: docs/specs/multi-machine-replicated-store-foundation.md §6 (snapshot-then-
 * tail), §6.1 (single-origin anti-forgery), §6.2 (snapshot format + watermark
 * VECTOR), §6.3 (the cutover apply path), §6.4 (HLC demoted to SECONDARY dedup),
 * §6.5 (tombstone high-water seed), §6.6 (why a per-(origin,kind) seq-watermark
 * vector, not a scalar), §8.2 (snapshot-cache fixed ceiling).
 *
 * GENERIC foundation machinery ONLY — there is NO concrete store kind here
 * (preferences/relationships are later consumers, WS2.1+). Everything ships DARK
 * behind `multiMachine.stateSync.<store>.enabled` (default false); a single-machine
 * agent (no peers to pull from) is a strict no-op.
 *
 * This module is PURE LOGIC. The 67MB-class whole-store materialization happens OFF
 * the event loop in `storeSnapshotBuild.worker.ts` (the instar#1069 requirement,
 * mirroring CartographerSweepEngine / cartographerDetect.worker.ts) — this file
 * exports the pure `materializeSnapshot()` the worker dispatches to, plus the
 * non-worker cache/breaker/cutover seams that run on the main thread but do only
 * bounded, O(records-in-one-batch) work. No fs, no Date directly, no network.
 *
 * THE CORRECTNESS BORROW (§6.3): the whole no-gap/no-double-apply guarantee is
 * inherited from the EXISTING seq-contiguous transport. This module:
 *   1. builds a single-origin snapshot (origin === serving machine, §6.1),
 *   2. computes a per-(origin,kind) seq-watermark VECTOR from the entries that
 *      actually materialized (§6.2 — it cannot lie),
 *   3. seeds `PeerMeta.kinds[kind].lastHeldSeq = snapshotSeq` (§6.3 step 3),
 *   4. then TAILS via the UNCHANGED `buildServeBatch(kind, snapshotSeq, M)` — the
 *      existing applier seq-contiguity (lastHeldSeq+1) does the gap/dup work.
 * There is NO new gap-detection here. HLC is a SECONDARY dedup hint only (§6.4).
 */
import { type HlcTimestamp } from './HybridLogicalClock.js';
/**
 * One materialized record in a single-origin snapshot (§6.2). Carries its `hlc`
 * (the merge total order), `op` (put/delete tombstone), `recordKey`, `origin`
 * (=== the serving machine, §6.1), and the opaque store-specific `data`. A delete
 * is a TOMBSTONE record (kept within the tombstone horizon, §6.5), never a physical
 * removal — so the apply path can resolve a delete↔put race deterministically.
 */
export interface SnapshotRecord {
    recordKey: string;
    hlc: HlcTimestamp;
    op: 'put' | 'delete';
    origin: string;
    /** The store-specific portion of the journal `data` (envelope fields stripped). */
    data: Record<string, unknown>;
}
/**
 * The per-(origin,kind) seq-watermark — a VECTOR component, not a scalar (§6.6).
 * `snapshotSeq` is the highest journal `seq` of an origin-M-authored entry that
 * materialized into the snapshot. It is THE TAIL CURSOR: the cutover seeds
 * `lastHeldSeq = snapshotSeq` then tails `buildServeBatch(kind, snapshotSeq, M)`.
 */
export interface SnapshotKindWatermark {
    /** The highest seq this snapshot materialized from for (origin, kind). */
    snapshotSeq: number;
}
/**
 * The snapshot's seq-watermark — a per-(origin,kind) VECTOR (§6.2 / §6.6). One
 * snapshot is single-origin, so `origin` is a scalar; the VECTOR is over the
 * contributing KINDS (a store may ride more than one journal kind). `maxHlc` is
 * SECONDARY (§6.4): an idempotency/dedup hint at cutover, NEVER the tail cursor.
 */
export interface SnapshotWatermark {
    /** === the serving machine (single-origin, §6.1). */
    origin: string;
    /** Per contributing (origin, kind) stream: the highest seq materialized. */
    kinds: Record<string, SnapshotKindWatermark>;
    /** SECONDARY dedup hint only — the max HLC over all records (§6.4). */
    maxHlc: HlcTimestamp;
}
/**
 * A built single-origin snapshot of store S from origin M (§6.2): the CURRENT
 * materialized state (latest record per recordKey; deletes tombstoned), plus the
 * watermark vector. `deleteWatermarks` carries the per-recordKey deleted-keys
 * high-water SEED (§6.5) — the HLC of the highest delete the snapshot reflects per
 * key — so the receiver can seed its resurrection guard from the snapshot.
 */
export interface StoreSnapshot {
    /** The store key (config sub-key / advert suffix, e.g. 'pref'). */
    store: string;
    /** === the serving machine (single-origin invariant, §6.1). */
    origin: string;
    /** The materialized records (latest per recordKey, deletes tombstoned). */
    records: SnapshotRecord[];
    /** The per-(origin,kind) seq-watermark VECTOR + the secondary maxHlc (§6.2). */
    watermark: SnapshotWatermark;
    /**
     * The tombstone high-water SEED (§6.5): per recordKey that is currently a
     * tombstone in this snapshot, the HLC of that delete. A receiver seeds its
     * deleted-keys high-water from these so a stale pre-delete put cannot resurrect
     * the key even after the tombstone record itself rotates out.
     */
    deleteWatermarks: Record<string, HlcTimestamp>;
    /** Total serialized byte size — for the cache byte ceiling (§8.2). */
    sizeBytes: number;
    /**
     * True iff the materialization hit `maxSnapshotBytes` and DROPPED records while
     * keeping the full watermark (§6.3 boundedness). A truncated snapshot is a
     * SUB-WATERMARK GAP TRAP: it seeds `lastHeldSeq = snapshotSeq` but does NOT
     * contain every record at-or-below that seq, so the subsequent tail (which serves
     * only `seq > snapshotSeq`) would NEVER replay the dropped records — a silent gap
     * the seq-contiguity cannot catch (it starts above them). Therefore a truncated
     * snapshot is structurally REFUSED, never applied: `applySnapshotCutover` throws on
     * it and `StoreSnapshotEngine.serveSnapshot` returns `build-truncated` so the caller
     * falls back to a from-genesis tail (the safe, complete path). The flag travels ON
     * the snapshot (not just the serve-result envelope) so the refusal cannot be
     * bypassed by a consumer that consumes a bare `StoreSnapshot`.
     */
    truncated: boolean;
}
/**
 * The minimal journal-entry shape the materializer reads. Matches `JournalEntry`
 * (CoherenceJournal) but typed locally so this module stays import-light and the
 * worker can pass plain objects across the thread boundary. `data` must carry the
 * replicated-record envelope fields (recordKey/hlc/op/origin) — already validated
 * by the applier before they landed; the materializer re-narrows defensively.
 */
export interface RawJournalEntry {
    seq: number;
    ts: string;
    machine: string;
    kind: string;
    data: Record<string, unknown>;
}
/** Input to the (worker-dispatched) pure materializer. */
export interface MaterializeInput {
    /** The store key (advert suffix). */
    store: string;
    /** The single origin (=== serving machine, §6.1) this snapshot is built for. */
    origin: string;
    /**
     * The own-stream entries to materialize, keyed by journal kind. EVERY entry MUST
     * have `entry.machine === origin` (the §6.1 single-origin invariant) — the
     * materializer DROPS (counts) any cross-origin entry rather than trusting it, so
     * a buggy caller cannot smuggle a foreign-origin record into a single-origin
     * snapshot. Within a kind, order does not matter — the materializer resolves by
     * HLC-max per recordKey.
     */
    entriesByKind: Record<string, RawJournalEntry[]>;
    /**
     * The per-kind byte ceiling for the materialized snapshot. A materialization
     * that would exceed it is TRUNCATED deterministically (highest-seq-first kept)
     * and the result is flagged `truncated` — bounded, never an unbounded build
     * (instar#1069). 0/absent ⇒ no per-build truncation (the cache byte ceiling
     * still bounds what is RETAINED).
     */
    maxSnapshotBytes?: number;
}
/** Result of the pure materializer (what the worker posts back). */
export interface MaterializeResult {
    snapshot: StoreSnapshot;
    /** Count of entries dropped for `entry.machine !== origin` (anti-forgery, §6.1). */
    crossOriginDropped: number;
    /** Count of entries dropped for a malformed/missing envelope (defensive re-narrow). */
    malformedDropped: number;
    /** True iff the materialization hit `maxSnapshotBytes` and was truncated. */
    truncated: boolean;
}
/**
 * Materialize a single-origin snapshot (§6.1 + §6.2). PURE — no I/O, no Date, no
 * network. This is the function the worker dispatches to (the heavy walk runs on
 * the worker thread).
 *
 * Algorithm:
 *  1. For every contributing kind, fold the own-stream entries into a per-recordKey
 *     LATEST record (HLC-max via HybridLogicalClock.compare). A cross-origin entry
 *     (`entry.machine !== origin`) is DROPPED + counted — the §6.1 anti-forgery
 *     invariant enforced at materialization, not trusted to the caller.
 *  2. Track the per-(origin,kind) `snapshotSeq` = the highest origin-authored
 *     `entry.seq` reflected (§6.2 — computed from what materialized, cannot lie).
 *  3. Track the per-recordKey delete high-water (§6.5) for the seed.
 *  4. Emit the materialized set (latest per key, deletes tombstoned) + the
 *     watermark vector + the secondary maxHlc.
 *
 * Boundedness (instar#1069): the per-key fold is O(entries); the result is the
 * live key count (≤ entries). `maxSnapshotBytes` deterministically truncates a
 * pathologically-large materialization (highest-seq-first kept).
 */
export declare function materializeSnapshot(input: MaterializeInput): MaterializeResult;
/** A cache key (§8.2): keyed by (origin, store, maxHlc). Deterministic string. */
export declare function snapshotCacheKey(origin: string, store: string, maxHlc: HlcTimestamp): string;
/**
 * The snapshot cache — a FIXED-ceiling ring (§8.2), NOT pool-size-scaled. Bounded
 * by BOTH `maxCachedSnapshots` (count) AND `maxCacheBytes` (bytes), whichever
 * binds first; LRU eviction with a monotonic `cacheLossCounter` (mirroring the
 * quarantine ring's lossCounter). An evicted snapshot is just rebuilt on next
 * demand (breaker-gated, §6.3) — eviction is a recompute, never a correctness
 * loss; the counter makes the recompute visible in degradation.
 *
 * A stale entry (its source stream advanced past the cached maxHlc) is dropped on
 * the next put for the same (origin, store) — the cache key embeds maxHlc, so a
 * fresher build is a DIFFERENT key; `invalidateOlder` drops the superseded ones.
 */
export declare class SnapshotCache {
    private readonly maxCount;
    private readonly maxBytes;
    private readonly entries;
    private clock;
    private bytes;
    private _cacheLossCounter;
    constructor(opts: {
        maxCachedSnapshots: number;
        maxCacheBytes: number;
    });
    /** Monotonic count of LRU evictions (§8.2 — surfaced in degradation). */
    get cacheLossCounter(): number;
    /** Current live entry count. */
    get size(): number;
    /** Current total cached bytes. */
    get byteSize(): number;
    /** Get a cached snapshot by key (refreshes its LRU position). */
    get(key: string): StoreSnapshot | undefined;
    /**
     * Put a freshly-built snapshot. Drops any STALE entry for the same (origin,
     * store) whose maxHlc is older than this one (the source stream advanced past
     * it, §8.2) — those are superseded, not LRU-evicted (no lossCounter bump). Then
     * enforces the count + byte ceilings via LRU eviction (lossCounter bumped).
     */
    put(snapshot: StoreSnapshot): void;
    /** Drop cached entries for the same (origin, store) with a strictly-older maxHlc. */
    private invalidateOlder;
    /** Evict LRU entries until BOTH the count and byte ceilings hold (§8.2). */
    private evictToBounds;
    /** Iterate cached snapshots for a given (origin, store) — bounded by maxCount.
     *  Does NOT touch LRU (a read-only scan; the caller touches LRU on a real serve). */
    entriesFor(origin: string, store: string): Iterable<StoreSnapshot>;
    /** Drop ALL cached entries for an origin (the §7.4 rollback-unmerge hook). */
    dropOrigin(origin: string): void;
}
/** Options for the rebuild breaker (§6.3 minimum-rebuild window + frequency cap). */
export interface RebuildBreakerOptions {
    /** Minimum ms between actual rebuilds for one (peer, origin, store) — a flapping
     *  peer is served the CACHED snapshot within this window. Default 30s. */
    minRebuildIntervalMs?: number;
    /** Max rebuilds allowed within `windowMs` before the breaker trips. Default 5. */
    maxRebuildsPerWindow?: number;
    /** The rolling window for the frequency cap. Default 5min. */
    windowMs?: number;
    /** How long the breaker stays tripped once it fires. Default 1min. */
    cooldownMs?: number;
}
/** A rebuild decision (§6.3): allow a fresh build, or serve the cache / refuse. */
export type RebuildDecision = {
    allow: true;
} | {
    allow: false;
    reason: 'within-min-interval' | 'breaker-open';
    serveCache: boolean;
};
/**
 * Per-peer snapshot-build-frequency breaker (§6.3). Prevents rebuild storms from a
 * flapping peer: within `minRebuildIntervalMs` the cached snapshot is served; more
 * than `maxRebuildsPerWindow` actual rebuilds in `windowMs` trips a breaker that
 * stays open for `cooldownMs`. Bounded (No-Unbounded-Loops). PURE-ish: the clock
 * is injected so it is unit-testable across simulated windows.
 *
 * Keyed by (peer, origin, store) so one peer flapping on store A does not throttle
 * its store-B rebuilds, and two peers requesting the same snapshot are independent.
 */
export declare class SnapshotRebuildBreaker {
    private readonly minIntervalMs;
    private readonly maxPerWindow;
    private readonly windowMs;
    private readonly cooldownMs;
    private readonly now;
    /** Per key: the recent rebuild timestamps (bounded by the window) + breaker state.
     *  `lastRebuildAt === null` means "never rebuilt" — distinct from a real now()=0
     *  timestamp, so the min-interval check is correct even when the clock starts at 0. */
    private readonly state;
    constructor(opts: RebuildBreakerOptions & {
        now: () => number;
    });
    private keyOf;
    /**
     * Decide whether a fresh rebuild is allowed RIGHT NOW for (peer, origin, store).
     * Returns `{ allow: true }` to build; otherwise a refusal with `serveCache`
     * (true = serve the cached snapshot instead of building). This does NOT record
     * the rebuild — call `recordRebuild()` after an ACTUAL build completes, so a
     * cache-served request never counts against the frequency cap.
     */
    shouldRebuild(peer: string, origin: string, store: string): RebuildDecision;
    /** Record that an actual rebuild completed for (peer, origin, store). */
    recordRebuild(peer: string, origin: string, store: string): void;
    /** Is the breaker currently open for (peer, origin, store)? (for tests/observability). */
    isOpen(peer: string, origin: string, store: string): boolean;
}
/**
 * The receiver-side seams the cutover needs from the existing applier — injected
 * so the cutover is unit-testable with in-memory fakes and never duplicates the
 * applier's seq-contiguity logic (§6.3: "rides the EXISTING seq transport"). The
 * real wiring binds these to JournalSyncApplier's PeerMeta + buildServeBatch.
 */
export interface CutoverApplierSeams {
    /**
     * Apply ONE materialized snapshot record into store S's (origin M) replicated
     * namespace via a per-recordKey HLC-max merge (§6.3 step 3). Returns whether the
     * record landed as the new winner (false = an already-present newer record was
     * not overwritten — idempotent). A `put` whose hlc is below the deleted-keys
     * high-water for its key is the receiver's resurrection-drop (§6.5 guard 1) and
     * returns false.
     */
    applySnapshotRecord(store: string, origin: string, rec: SnapshotRecord): boolean;
    /**
     * SEED, per contributing (origin, kind) stream, `PeerMeta.kinds[kind].lastHeldSeq
     * = snapshotSeq` (§6.3 step 3 — the load-bearing cursor placement). Idempotent:
     * re-seeding to the same seq is a no-op; the real applier never LOWERS a
     * lastHeldSeq it already advanced past (a re-cutover with a stale snapshot does
     * not rewind the cursor).
     */
    seedLastHeldSeq(origin: string, kind: string, snapshotSeq: number): void;
    /**
     * Seed the per-recordKey deleted-keys high-water from the snapshot (§6.5). The
     * receiver MERGES (HLC-max) — never lowers an existing high-water.
     */
    seedDeleteWatermark(store: string, origin: string, recordKey: string, hlc: HlcTimestamp): void;
    /**
     * Record an HLC-identity (recordKey, origin, hlc) as already-merged (the §6.4
     * SECONDARY dedup set). Returns whether the identity was NEW (false = already
     * seen ⇒ the caller drops the duplicate). This is a redundant safety net layered
     * ON the seq contiguity, never a substitute for it.
     */
    recordHlcIdentity(store: string, recordKey: string, origin: string, hlc: HlcTimestamp): boolean;
}
/** The verdict for one applied snapshot record (for the cutover result tally). */
export interface CutoverApplyTally {
    /** Records that landed as the new winner. */
    applied: number;
    /** Records skipped as an HLC-identity duplicate (the §6.4 secondary net). */
    dedupSkipped: number;
    /** Records the applier did not land (older than the present winner, or a
     *  resurrection drop — §6.5). */
    notWinner: number;
}
/**
 * Apply a single-origin snapshot at cutover (§6.3 steps 3–5 + §6.4 + §6.5). This
 * is the load-bearing seam: it (a) applies each record via HLC-max merge through
 * the injected applier, (b) seeds the deleted-keys high-water from the snapshot,
 * (c) seeds `lastHeldSeq = snapshotSeq` per contributing kind so the NEXT ordinary
 * tail is already in-contiguity, and (d) uses the §6.4 secondary HLC-identity set
 * as a belt-and-suspenders dedup.
 *
 * It does NOT do the tail — that is the UNCHANGED `buildServeBatch(kind,
 * snapshotSeq, M)` path the caller drives next. There is NO gap-detection here;
 * the seq contiguity already there does the work (§6.3 step 4).
 *
 * Idempotency (§6.3 step 5): re-running is safe — re-seeding the cursor to the
 * same snapshotSeq + re-applying is a per-recordKey HLC-max merge (an
 * already-present newer record is not overwritten) and the seq tail drops
 * at-or-below entries as duplicates.
 *
 * PURE-ish: all state mutation is funnelled through the injected seams.
 */
export declare function applySnapshotCutover(snapshot: StoreSnapshot, seams: CutoverApplierSeams): CutoverApplyTally;
/**
 * Compute the tail request cursor for a contributing (origin, kind) AFTER a
 * cutover (§6.3 step 4). The tail rides the UNCHANGED transport:
 * `buildServeBatch(kind, snapshotSeq, origin)`. This helper is the single place
 * that names the contract — the cursor is the snapshot's per-kind `snapshotSeq`,
 * NOTHING HLC-derived (HLC is demoted to secondary dedup, §6.4).
 */
export declare function tailCursorAfterCutover(snapshot: StoreSnapshot, kind: string): number;
/** Validate + narrow an untrusted wire snapshot (the receiver side of the mesh
 *  verb). Returns null to REJECT the whole snapshot (single-origin violated, a
 *  malformed record, etc.) — the §6.1 anti-forgery + §6.2 format gate at the door.
 *  `expectedOrigin` is the AUTHENTICATED sender (the mesh layer proved it); a
 *  snapshot whose `origin` or any record `origin` disagrees is rejected wholesale
 *  (the cross-origin-snapshot attack, §6.1). */
export declare function validateWireSnapshot(raw: unknown, expectedOrigin: string): StoreSnapshot | null;
/** Result of a snapshot-serve request (§6.3): the snapshot + how it was produced. */
export type SnapshotServeResult = {
    ok: true;
    snapshot: StoreSnapshot;
    source: 'built' | 'cache';
    durationMs: number;
    truncated: false;
} | {
    ok: false;
    reason: 'breaker-open' | 'build-failed' | 'build-timeout' | 'no-entries' | 'build-truncated';
    durationMs: number;
};
/** The seams the engine needs to LOAD a store's own-stream entries (injected so
 *  the engine is testable without touching disk; the real wiring reads the
 *  CoherenceJournal own streams for the contributing kinds). */
export interface SnapshotEngineSeams {
    /** Load the own-stream entries for (origin, store), keyed by contributing kind.
     *  The caller (real wiring) returns ONLY this machine's own entries (single-
     *  origin, §6.1); the materializer ALSO enforces it (drops cross-origin). */
    loadOwnEntries(store: string, origin: string): Record<string, RawJournalEntry[]>;
    /** Injected wall clock (ms) — for the breaker + duration. */
    now(): number;
}
/** Engine construction options. */
export interface StoreSnapshotEngineOptions {
    cache: SnapshotCache;
    breaker: SnapshotRebuildBreaker;
    seams: SnapshotEngineSeams;
    /** Per-build worker timeout (ms). Default 120s (mirrors CartographerSweepEngine). */
    buildTimeoutMs?: number;
    /** Worker heap ceiling (MB). Default 1536 (mirrors CartographerSweepEngine). */
    workerHeapMb?: number;
    /** The per-build snapshot byte ceiling passed to the materializer (deterministic
     *  truncation). Default 0 ⇒ no per-build truncation (cache byte ceiling still
     *  bounds retention). */
    maxSnapshotBytes?: number;
    /** Optional structured logger for build observability (default no-op). */
    log?: (event: string, detail: Record<string, unknown>) => void;
    /** Test seam: run the materializer INLINE instead of spawning a worker (the unit
     *  tests assert the cache/breaker orchestration without a real thread). Default
     *  false ⇒ the real off-event-loop worker is spawned (the instar#1069 path). */
    runInline?: boolean;
}
/**
 * StoreSnapshotEngine — the §6.3 serve orchestrator. For a (peer, origin, store)
 * request it:
 *   1. asks the rebuild breaker whether a fresh build is allowed (a flapping peer
 *      is served the cached snapshot; a tripped breaker refuses — §6.3),
 *   2. on a build: loads the own-stream entries (single-origin), runs the
 *      materializer OFF THE EVENT LOOP in a worker thread (instar#1069), bounded
 *      by `buildTimeoutMs`, caches the result (LRU + cacheLossCounter, §8.2), and
 *      records the rebuild against the breaker,
 *   3. on a within-window / breaker-open decision: serves the cached snapshot if
 *      present, else refuses (the caller falls back / retries later).
 *
 * The HEAVY work (whole-store materialization) is the worker's; the engine itself
 * does only bounded orchestration on the main thread.
 */
export declare class StoreSnapshotEngine {
    private readonly cache;
    private readonly breaker;
    private readonly seams;
    private readonly buildTimeoutMs;
    private readonly workerHeapMb;
    private readonly maxSnapshotBytes;
    private readonly log;
    private readonly runInline;
    constructor(opts: StoreSnapshotEngineOptions);
    /** The snapshot cache (for observability / the rollback-unmerge dropOrigin hook). */
    getCache(): SnapshotCache;
    /**
     * Serve a single-origin snapshot of (origin, store) to `peer` (§6.3). Builds off
     * the event loop, reuses the cache within the rebuild window, breaker-gated.
     */
    serveSnapshot(peer: string, origin: string, store: string): Promise<SnapshotServeResult>;
    /** The most-recent cached snapshot for (origin, store), if any. */
    private mostRecentCached;
    /** Run the materializer — off the event loop (worker), or inline for tests. */
    private runBuild;
    /** Minimal env allowlist for a spawned worker — NEVER the parent process.env. */
    private workerEnv;
    /** Spawn the build worker, await its single message, bound by buildTimeoutMs. */
    private runWorker;
}
//# sourceMappingURL=StoreSnapshot.d.ts.map