import { type MemorySyncCursor } from "./sync.js";
import { type MemoryBackend, type MemoryEntry, type PatchReport } from "./types.js";
/** The injected HTTP seam — core never bundles a fetch. The deployment maps `path` (e.g.
 *  `/v1/memory/sync/user%3Aalice`) onto its server base URL, attaches auth, and returns the
 *  DECODED JSON body of a 2xx response; any non-2xx / network failure should THROW (the round then
 *  aborts with zero local side effects — the transport runs before any local write). */
export type MemorySyncTransport = (path: string, body: unknown) => Promise<unknown>;
/** The wire REQUEST of one round (the server's `parseMemorySyncRequest` twin): the push
 *  half as computed against the client's own cursor. `deletes` ⊆ `Object.keys(baseRevs)` by
 *  construction (a delete is CAS'd on its baseline; an id never synced cannot be delete-propagated). */
export interface MemorySyncRequestBody {
    peer: string;
    /** id → rev at the last completed round (the cursor's baseRevs; empty = first round). */
    baseRevs: Record<string, string>;
    /** Full state of every entry new/changed here since the baseline. When
     *  {@link SyncMemoryScopeOptions.maxPushEntries} is set this is the id-ascending PREFIX of the
     *  full push set (batching — the remainder rides later rounds). */
    entries: MemoryEntry[];
    /** Ids deleted here since the baseline (tombstoned or gone from the local plane). Deletes are
     *  NEVER batched/split — they are id+baseRev pairs, weightless next to entry bodies. */
    deletes: string[];
    /** pull-half batching (wire contract; server implements the response side): ask the
     *  server to cap this round's pull half at `limit` items. Absent ⇔ unbatched (the prior
     *  wire shape byte-for-byte — old servers never see the key). */
    pull?: {
        limit?: number;
    };
}
/** The wire RESPONSE of one round (server `MemorySyncResponse` twin: one RTT, both
 *  directions; `serverDeletes` is the delete-propagation leg [without it a central
 *  removal never crosses = livelock]; `cursor` is the server-computed next baseline with the
 *  retention discipline ALREADY applied to the pull half). */
export interface MemorySyncResponseBody {
    /** Ops the server actually landed from our push (empty on an idempotent replay). */
    applied: PatchReport["applied"];
    /** Server-side rejections/divergences: inbound-gate rejects, rev lies, CAS losses, both-sides
     *  divergence (resolve via the §3 ladder — loser minted as a sibling, never dropped). */
    conflicts: Array<{
        id: string;
        reason: string;
        baseRev?: string;
        currentRev?: string;
    }>;
    /** The pull half: entries new/changed centrally since our baseline. */
    serverEntries: MemoryEntry[];
    /** The pull half's delete leg: centrally deleted, unchanged here since baseline (CAS baseRev). */
    serverDeletes: Array<{
        id: string;
        baseRev: string;
    }>;
    /** The next baseline to persist AFTER the pull half lands (the client cannot compute it — it
     *  does not hold the central set). Pull-half ids are retained at the OLD baseline inside; the
     *  client advances the LANDED ones to their landed state before persisting (see ⑤ — otherwise
     *  every landed pull echoes back next round and a central delete can be resurrected). */
    cursor: {
        peer: string;
        baseRevs: Record<string, string>;
        updatedAtMs: number;
    };
    /** pull-half batching (wire contract; server sets it): true ⇔ the server withheld part
     *  of the pull half under the requested `pull.limit` — the caller should run another round.
     *  The server MUST retain every withheld id at the OLD baseline inside `cursor` (the client
     *  refuses prefilled undelivered revs — see ⑤ defense — so violating this only costs the
     *  server an ignored advance plus a disclosed warning, never a permanently-missed pull). */
    pullTruncated?: true;
}
/** One aggregated conflict: `server` = reported by the central half (its gate/CAS/divergence),
 *  `local` = produced landing the pull half here (our CAS refusals, inbound-gate rejects, rev
 *  lies). Nothing is hidden; the caller resolves via the §3 ladder (loser-as-sibling). */
export interface MemorySyncClientConflict {
    side: "server" | "local";
    id: string;
    reason: string;
    baseRev?: string;
    currentRev?: string;
}
export interface SyncMemoryScopeOptions {
    /** The LOCAL authority plane (TOC: the File backend). */
    backend: MemoryBackend;
    /** The scope this round syncs (one round = one scope, mirroring the server route). */
    scope: string;
    /** The injected HTTP seam. */
    transport: MemorySyncTransport;
    /** The persisted cursor from the last completed round with this peer; absent = first round. */
    cursor?: MemorySyncCursor;
    /** Peer name for the cursor partition (defaults to the cursor's peer, else "central"). */
    peer?: string;
    /** Caller-injected clock (engine precedent) — stamps the returned cursor's updatedAtMs. */
    now?: () => number;
    /** push-half batching: cap the number of push-half ENTRIES sent this round. The full
     *  push set is put in STABLE order (id ascending — deterministic across rounds/replicas) and the
     *  first `maxPushEntries` ride the wire; a cut round reports `pushTruncated: true`. Deletes are
     *  never split (id+baseRev pairs are weightless next to bodies). CALLER CONTRACT: on
     *  `pushTruncated`/`pullTruncated`, persist the returned cursor and run ANOTHER round until both
     *  flags clear — core deliberately never loops internally (the runner loop is the deployment's,
     *  server-side semantics apply); bound your loop (e.g. a fixed max-rounds cap ≥
     *  ⌈expected entries / limit⌉ + slack) so a misbehaving peer cannot spin it forever. Must be a
     *  POSITIVE integer (≥ 1 — C5: a 0 cap would flag pushTruncated forever and livelock the caller
     *  loop); absent ⇔ unbatched (prior behavior byte-for-byte). */
    maxPushEntries?: number;
    /** pull-half batching: forwarded verbatim as the wire request's `pull.limit`; a server
     *  that withholds part of its pull half answers `pullTruncated`, passed through on the result.
     *  Same caller contract as {@link maxPushEntries}: continue rounds (bounded) until it clears.
     *  Must be a POSITIVE integer (≥ 1 — C5, same livelock gate); absent ⇔ the request carries NO
     *  `pull` key (old-server compatible, pre- wire bytes). */
    maxPullEntries?: number;
    /** design/178 §3 — per-entry byte bound for the PUSH gate, matched against the entry's serialized
     *  file form (the same domain the harvest gate measures). Defaults to {@link MAX_MEMORY_BYTES},
     *  the engine's own per-file cap, so the wire cannot carry an entry the local gate would have
     *  refused: `scanMemoryWrite`'s size rung only runs when it is given a bound, and an entry that
     *  reached the backend out of band is exactly the population this gate exists for. */
    maxEntryBytes?: number;
}
export interface MemorySyncClientResult {
    /** True ⇔ the round was fully clean: cursor advanced AND zero conflicts on either side. */
    ok: boolean;
    /** What the push half contained (ids only — the bodies already went over the wire). */
    pushed: {
        entries: string[];
        deletes: string[];
    };
    /** Ops the server landed from our push (response `applied` verbatim). */
    serverApplied: PatchReport["applied"];
    /** Ops we landed locally from the pull half. */
    localApplied: PatchReport["applied"];
    /** Aggregated server-reported + local conflicts (never hidden). */
    conflicts: MemorySyncClientConflict[];
    /** True ⇔ every pull-half item landed cleanly, so `cursor` is the NEW baseline to persist. */
    cursorAdvanced: boolean;
    /** The cursor the caller should now persist: advanced on full success, otherwise the INPUT
     *  cursor unchanged (undefined on a failed first round) — the retention discipline. */
    cursor: MemorySyncCursor | undefined;
    /** present (true) ⇔ `maxPushEntries` cut this round's push set — run another round off
     *  the persisted cursor until it clears (see the {@link SyncMemoryScopeOptions.maxPushEntries}
     *  caller contract). Never present on an uncut round (additive: old callers see no new key). */
    pushTruncated?: true;
    /** present (true) ⇔ the server reported its pull half was cut under `pull.limit` —
     *  same continue-until-clear caller contract. */
    pullTruncated?: true;
    /** Honest-disclosure channel for tolerated protocol violations (present only when non-empty).
     *  Today's single producer is the ⑤ cursor-prefill defense: a server that advances a
     *  NOT-DELIVERED-HERE id's rev inside the response cursor gets that advance REFUSED (old
     *  baseline kept / unknown id dropped) and disclosed here — silently swallowing it would turn
     *  the next round's pull judgment into a permanent miss (baseline == central rev ⇒ never
     *  pulled) or, for an unknown id, a spurious push-half DELETE of an entry never seen here. */
    warnings?: string[];
}
/**
 * One full CLIENT sync round for one (scope, peer) against the central sync API — see the
 * module header for the ①-⑤ flow and the cursor retention discipline. Pure orchestration: all I/O
 * rides the injected backend/transport; the caller persists the returned cursor.
 */
export declare function syncMemoryScope(opts: SyncMemoryScopeOptions): Promise<MemorySyncClientResult>;
