import type { ExecutionEnv } from "../../internal/harness-types.js";
import { type FileHistoryDiffStats, type FileHistoryExport, type FileHistoryResult, type FileHistoryRestoreResult, type FileHistoryStore, type FileHistoryTrackResult } from "../../core/file-history-store.js";
import { type FileHistoryRetentionPolicy } from "../../core/file-history-retention.js";
import { type CorruptReadInfo } from "./corrupt-read-sink.js";
/**
 * design/381 slice 2 — the DURABLE file-backed {@link FileHistoryStore}: the per-edited-file rewind
 * history that survives a process restart (slice 1 shipped the contract + the in-memory reference,
 * and the file backend carried no rewind store in between — this closes that hole).
 *
 * It REUSES core's exported halves rather than re-deriving them, which is the §3 分工 clause, not a
 * style preference: `captureFileBackupViaEnv`/`applyFileRestoreViaEnv` own every env-touching and
 * security-relevant decision (content-hash equality, deletion markers, the parent-identity guard),
 * and `trackKeyOf`/`resolveTrackKey` own the path coordinate (root-relative under the task root,
 * verbatim-absolute outside it, family-conditional, '..' fail-closed). A backend that hand-rolls
 * either is how symlink-escape and moved-root bugs come back.
 *
 * ON-DISK LAYOUT under `<root>/file-history/`:
 * ```
 *   blobs/<sha256>                                write-ONCE content-addressed bytes, shared by
 *                                                 every scope and version (EEXIST = dedup no-op)
 *   scopes/<sanitizeScope(scope)>/
 *       tracked.json                              the tracked set + version chains (pair array)
 *       boundaries/<entryId>.json                 one COMMITTED boundary = one file (its existence
 *                                                 IS canRestore; written LAST, so a boundary is
 *                                                 visible only once everything it references is)
 *       staging/<attempt>.jsonl                   the crash-residue journal of an in-flight capture
 * ```
 *
 * ATOMIC VISIBILITY (§3, the F5/G11a family): blobs are published first (atomic write-then-link),
 * the version chains second (one atomic whole-file replace), the boundary file LAST. A crash in the
 * middle therefore leaves at worst orphan blobs and orphan version records — both collectable — and
 * NEVER a boundary whose bytes are missing. `canRestore` only ever answers true for a boundary file
 * that exists and parses.
 *
 * CRASH RESIDUE IS REAPER-JUDGEABLE (the slice-1 adversarial round's registered leak, closed here):
 * an in-memory backend that dies mid-capture leaks its staged blobs with nothing left to judge them.
 * Here every capture opens a staging journal naming the blobs it published; `reap` unions the
 * journals of THIS process's live attempts into the live set (so a concurrent reap cannot collect a
 * capture's bytes) and treats every OTHER journal as residue of a dead attempt — it removes the
 * journal, and the blobs it protected fall out of the live set in the same pass.
 *
 * WRITER COORDINATES (stated rather than assumed; the two clauses answer different questions):
 *  · WITHIN one process, the durable face is the FILE, not the instance — several store instances
 *    over one data root are a supported configuration (this suite pins it): committed history is
 *    mutually visible, verb bodies re-read disk after every await and commit synchronously (one
 *    event loop = no torn interleave), and `reap` judges another instance's in-flight staging by
 *    proof-of-death, never by "not in my map". There is deliberately no shared in-process table —
 *    cross-instance coordination all rides the on-disk artifacts.
 *  · ACROSS OS processes the premise stays SINGLE-WRITER per data directory (the
 *    {@link import("./index.js").FileStorageBackend} boot lock in the assembled deployment; a
 *    directly-constructed store takes no fence of its own — the contract has no dispose verb a
 *    lease could ride): `tracked.json` is a whole-file replace with no cross-process merge, so two
 *    WRITER processes can lose each other's rows. Only the GC path is hardened beyond the premise
 *    (a foreign journal is protected by owner-pid liveness + age), because deleting bytes is the
 *    one mistake that cannot be walked back. The capture-lease sets
 *    (`spentAttempts`/`activeCaptures`) are deliberately in-process and NOT persisted (§10 S-6:
 *    the lease close is the calling Runner's discipline, and after a crash no new process has a
 *    capture path for the old entryId), so cross-replica fencing is a multi-writer backend's own
 *    job (the SQL twin's DB-level claim), not something this backend pretends to provide.
 *
 * RETENTION IS BUILT IN (design/381 §片2): a scope's boundary directory grows by one file per turn,
 * so this backend trims it after every commit — the newest
 * {@link import("../../core/file-history-retention.js").DEFAULT_FILE_HISTORY_BOUNDARY_KEEP} survive
 * by default, and {@link FileFileHistoryStoreOptions.retention} is how a deployment says otherwise
 * (including handing the schedule back with `keep: "unbounded"`). Of the two bundled backends this
 * is the one where the absence of a bound was a disk that fills, which is why the bound is the
 * store's own and not a chore delegated to whoever wires it.
 */
export interface FileFileHistoryStoreOptions {
    /**
     * Disclosure sink for a durable read this store treats as ABSENT because the bytes were corrupt
     * or unreadable — same name and `{path, reason}` shape as every sibling file store, because
     * "unreadable read as missing" is one failure grammar. Fires for: a corrupt/unparseable
     * `tracked.json` or boundary file collapsed to "no history" (the never-throw seam's documented
     * fail-open), and a blob-GC pass that ABORTED because its live set could not be completed (blobs
     * are then kept — leaked, never lost). Never fires on plain ENOENT: a scope with no history yet
     * really is empty. Swallow-guarded.
     */
    onCorruptRead?: (info: CorruptReadInfo) => void;
    /**
     * design/381 §片2 — the built-in boundary bound and its override. Omitted ⇒ the engine default
     * ({@link import("../../core/file-history-retention.js").DEFAULT_FILE_HISTORY_BOUNDARY_KEEP}
     * newest boundaries per scope, trimmed after each commit); `{ keep: "unbounded" }` switches the
     * self-trim off and hands the GC schedule back to the deployment. A malformed policy is REFUSED
     * at construction — this store writes to a disk that a silently-substituted retention policy
     * would fill.
     */
    retention?: FileHistoryRetentionPolicy;
}
export declare class FileFileHistoryStore implements FileHistoryStore {
    private readonly base;
    private readonly blobsDir;
    private readonly scopesDir;
    private readonly onCorruptRead;
    /**
     * scope → entryIds whose capture attempt failed: the lease is spent, so a retry refuses with ZERO
     * env reads (§10 S-6 — in-process, deliberately not persisted; see {@link reapedCache} for the
     * neighbouring ledger that IS durable and why the two differ).
     *
     * NESTED, not the joined `scope\0entryId` key this used to be (slice 3) — and the honest reason is
     * defence in depth rather than a live bug. A joined key is only unambiguous while NEITHER component
     * can contain the separator, and the entry id cannot: `historyPathComponent` runs FIRST in
     * `snapshot` and rejects anything outside `[A-Za-z0-9_.-]`, so with NUL-free ids the last separator
     * splits the string uniquely and no two (scope, entryId) pairs can share a key. The problem with
     * that argument is where it lives: the ledger's correctness depended on a whitelist in a different
     * function, and a future id spelling that relaxes it (or a second caller that reaches these ledgers
     * without the path guard) would silently make one scope's failed capture close another scope's
     * lease — refusing a good boundary AND explaining it with a "prior attempt" that scope never made.
     * The nested form does not need the argument. It is the shape the reaped ledger beside it already
     * uses, for the reason stated there.
     */
    private readonly spentAttempts;
    /** scope → entryId → staging journal path, for the CAPTURES in flight right now: a concurrent
     *  same-entryId capture is refused, and `reap` treats their journals' blobs as live. */
    private readonly activeCaptures;
    /** scope → the staging journals of the IMPORTS in flight right now. Separate from the captures
     *  above rather than sharing one key space with them: an import is keyed by its own journal path
     *  and a capture by an entry id, and one map holding both would be a key space where two unrelated
     *  vocabularies have to promise never to spell the same string. */
    private readonly activeImports;
    /**
     * The CONSUMED-ID ledger's in-process view — a CACHE over `scopes/<scope>/reaped.log`, not the
     * record itself (design/381 slice 3, closing the residual slice 2 registered here by name).
     *
     * Why it had to become durable. Reaping a boundary deletes the one artifact that proved its
     * entryId had been consumed, and `snapshot`'s create-once check read exactly that artifact; without
     * a tombstone a later capture for a reaped id takes the CAPTURE path and publishes post-turn bytes
     * under a pre-turn key. Slice 2 kept the tombstones in this object, which holds for one store
     * instance and for nothing else: a PEER store over the same data directory (supported in-process)
     * and every restart see neither the deleted boundary file nor another instance's Set, so an old
     * entryId re-presented THERE reached the capture path — the hole, one door over. The ledger is now
     * an append-only log per scope, so every reader of that directory judges the same ids.
     *
     * The cache is keyed by the log's SIZE, which is sound precisely because the log is append-only:
     * a peer's reap can only make it longer, so an unchanged size is an unchanged set. Steady state is
     * therefore one `stat` on the per-turn path, which is what makes the durability affordable — and
     * that sentence is load-bearing rather than decorative: past its bound a scope reaps on EVERY
     * commit, so an entry dropped at each append would make the steady state a full re-read of a log
     * that only grows. {@link markReaped} therefore CARRIES this entry across its own append.
     *
     * Growth is stated rather than hidden: one line (~40 bytes) per boundary ever reaped in the scope,
     * forever — the cheap residue of a bound that throws away complete tracked-set mappings, by two
     * orders of magnitude. It is what makes the bound SAFE rather than merely small.
     */
    private readonly reapedCache;
    /** Resolved boundary cap, or `null` for the explicit `"unbounded"` opt-out (host owns the GC). */
    private readonly boundaryKeep;
    /** Highest {@link BoundaryJson.committedAt} this instance has minted — see {@link nextCommittedAt}. */
    private lastCommittedAt;
    /** Scopes whose on-disk maximum has already been folded into {@link lastCommittedAt} — see there. */
    private readonly seededScopes;
    /**
     * The publication stamp, STRICTLY INCREASING — and increasing against what is already ON DISK, not
     * merely against what this object has minted (cross-review rounds 4 and 5).
     *
     * `Date.now()` has millisecond resolution and turns are faster than that, so several boundaries
     * could legitimately share a stamp — and a tied group falls through to the entry-id tie-break,
     * which is only commit order for MONOTONIC ids. A host minting arbitrary entry ids could therefore
     * have the retention pass drop the genuinely newer of two same-millisecond boundaries and keep the
     * older. Ties are not broken better here; they are made not to happen, which is the difference
     * between a heuristic and an order. (The same technique the engine's own uuidv7 mint uses.)
     *
     * The instance counter alone was not enough, and the reason is a configuration this backend
     * explicitly supports rather than an exotic one: a SECOND store object over the same directory — or
     * the same process restarted, which is the everyday case — starts its counter at zero, so after a
     * clock rollback (or simply within the same millisecond as the previous instance's last commit) it
     * could stamp a NEW boundary at or below an existing one, and retention would then keep the older.
     * So the first commit into a scope SEEDS the counter from that scope's highest resolved order.
     * Cost: one directory read per (instance, scope), amortized to nothing across a session.
     *
     * RESIDUAL, named rather than implied: two live instances interleaving commits into one scope
     * inside a single millisecond can still tie, because each advances its own counter after seeding.
     * A tie is then decided by the entry id, which IS commit order for engine-minted uuidv7 ids.
     */
    private nextCommittedAt;
    constructor(root: string, opts?: FileFileHistoryStoreOptions);
    /**
     * The RETENTION pass, run after a boundary file is published (design/381 §片2 — see
     * {@link FileHistoryStore.reap} for why the backend owns this rather than the host).
     *
     * COMMIT ORDER, in order of preference: the boundary's own {@link BoundaryJson.committedAt} stamp,
     * else the file's mtime, else the entry id. The layering is the point — each fallback is weaker
     * than the one above it and is named so nobody has to reverse-engineer which one is in force:
     *  · `committedAt` is written at publication and carried across `adoptScope`, so it survives the
     *    ordinary operations that destroy an mtime (a `cp` without `-p`, a restore from backup, the
     *    re-serialization adoption does). Absent only on records written before the field and on
     *    imported graphs, whose envelope carries no such value.
     *  · mtime is the fallback for exactly those, and is honest for a reason this family's standing
     *    law makes worth stating: DV-13 forbids metadata as a proof of CONTENT sameness, because
     *    deciding a tracked file "did not change" is a correctness and security judgment mtime cannot
     *    carry. Ordering records THIS store wrote once and never rewrites is a different question
     *    about a different object; being one place off costs a different-but-equally-legal retention
     *    choice and can never mis-restore a byte.
     *  · the entry id breaks ties, which for engine-minted uuidv7 ids IS commit order.
     * A MIXED population (old records with no stamp beside new ones that have it) sorts coherently for
     * a reason that is a fact rather than a hope, and is written down so nobody "fixes" one side into
     * a different unit: both sources are milliseconds since the epoch — `Date.now()` and `Stats.mtimeMs`
     * — so a stamped and an unstamped record are directly comparable.
     * The residual, stated rather than papered over: a clock stepped BACKWARDS between commits, or a
     * host minting non-monotonic entry ids of its own, can still make the pass drop a boundary that
     * was not the oldest. It cannot make the pass drop THIS turn's boundary —
     * {@link fileHistoryBoundariesToKeep} pins the just-committed id — and it cannot cost the pristine
     * v1 fallback, which `reap` exempts permanently.
     *
     * WHAT IT REFUSES TO JUDGE — FAIL-CLOSED, WHOLE (cross-review rounds 1+2): if ANY `.json` in the
     * scope fails to parse or fails its own digest, this pass does NOTHING for that scope. Not "skip
     * that file", not "keep it and trim around it": nothing. The intermediate form is a trap, and it
     * is worth writing down because it is the one this pass shipped with for a round. `reap` rebuilds
     * its version-reference set from the READABLE boundaries, so retaining an unreadable boundary
     * while trimming its neighbours still destroys the version records and blobs that only the
     * unreadable one referenced — the file survives as "evidence" of history whose bytes the same
     * pass deleted. And counting it toward the bound is the other half of the same mistake: one
     * damaged record would push a healthy, still-restorable boundary out of the window. A scope with a
     * record nobody can judge is a scope in a FAULT state, and an automatic, per-turn, irreversible
     * deletion pass is exactly the wrong thing to keep running there. It grows past its bound instead,
     * loudly (disclosed every pass), until an operator looks — the safe direction of a choice between
     * "too much disk" and "history destroyed". The one caveat a deployment has to hear: that
     * disclosure is only as loud as the sink it wired. A deployment that registers no
     * {@link FileFileHistoryStoreOptions.onCorruptRead} gets the unbounded growth WITHOUT the notice —
     * the same limitation every other corrupt-read fact in this store carries, and the reason the sink
     * is not optional equipment in a durable deployment.
     *
     * COST, stated rather than left to be measured: the common path is ONE `readdir` — a scope under
     * its bound parses nothing, stats nothing and calls no GC. A scope OVER the bound reads each
     * boundary file (validation, then its stamp) and runs the whole `reap` pass, which ends in the
     * cross-scope blob GC that walks every scope under this data root. Past the bound that is
     * per-commit work proportional to the root, not to this scope — the price of the bound, and the
     * reason the cheap path is kept genuinely cheap.
     *
     * Never throws and never fails the commit that triggered it (the boundary is already durable; a
     * GC that could not run leaves a graph larger than its bound, the safer of the two directions).
     */
    private applyBoundaryRetention;
    /** The single delivery point for the disclosure sink. */
    private disclose;
    private scopeDir;
    private trackedPath;
    private boundaryDir;
    /** A boundary file path, or null when `entryId` fails the strict path-component guard — the
     *  never-throw seam turns that into a typed result at the caller, never an exception. */
    private tryBoundaryPath;
    private blobPath;
    /**
     * Read a scope's tracked set, distinguishing the three states that matter (adversarial round 1):
     * ABSENT (no history yet — the ordinary empty case), READ (the graph), and CORRUPT (bytes exist
     * but cannot be understood). Collapsing CORRUPT into "empty" was a false-green machine: a readable
     * boundary beside an unreadable tracked set made `restore` iterate zero files and report
     * `complete` — a successful rewind that rewound nothing — and let the next `trackEdit` re-mint
     * possibly-already-edited bytes as pristine. Callers must branch on `corrupt`.
     */
    private readTrackedState;
    /** The tracked set alone, for the read paths that have already handled the corrupt state. */
    private readTracked;
    /** Replace a scope's tracked set in ONE atomic step (the DV-14 "one atomic durable record" and
     *  the version-chain commit both ride this). */
    private writeTracked;
    private readBoundary;
    /**
     * A committed boundary's own publication stamp ({@link BoundaryJson.committedAt}), or `undefined`
     * when the record predates the field, came from an import, or cannot be read. Callers treat
     * `undefined` as "no stamp" and fall back — never as a value, and never as a reason to refuse: a
     * boundary with no stamp is perfectly restorable, it just cannot state its own place in the order.
     */
    private boundaryCommittedAt;
    /**
     * A boundary's place in commit order, resolved through the whole fallback chain in ONE place so
     * the retention sort and the adoption copy cannot disagree about it (cross-review round 3): its
     * own stamp, else the file's mtime, else `undefined` when neither is available. Both sources are
     * milliseconds since the epoch, so a mixed population compares directly.
     */
    /**
     * UNION the source scope's consumed-id ledger into the target's — the adoption invariant, in ONE
     * place because it has to hold on EVERY successful arm of `adoptScope` and the arm it was first
     * missing from was the early one ("nothing to adopt", which is a statement about the GRAPH and not
     * about the ledger). A fork is the source's history continued under a new name, so an id the
     * source spent must not become mintable there.
     */
    /**
     * The boundary ids in `scope` that exist on disk but cannot be judged — `null` means the whole set
     * is judgeable and safe to reason about. A directory that cannot be LISTED answers with an empty
     * array rather than `null`, because "I cannot see them" and "I saw them and they are fine" must
     * not be the same answer; ENOENT (no boundaries yet) is genuinely the second.
     */
    private unjudgeableBoundaryIdsOf;
    private carryConsumedIds;
    /**
     * The consumed-id ledger's path — a SIBLING of the scope directory, deliberately NOT a file inside
     * it (cross-car review, adopted). `adoptScope` publishes by REPLACING the scope directory, and a
     * ledger that lives inside it dies with every such replacement: the version that did had to read
     * the ids out, carry them through the staging directory and write them back, which works right up
     * until a crash lands between the removal and the rename — at which point the directory (and the
     * only record of that scope's spent ids) is gone and an old entry id becomes mintable again.
     * Outside, the ledger is simply never moved: the publish cannot lose it, and the carry-and-rewrite
     * that used to be necessary is gone with it.
     *
     * A scope's sanitized directory name can never contain a `.` (non-alphanumerics collapse to `-`,
     * then a hash is appended), so the `.reaped.log` suffix cannot collide with another scope's dir.
     */
    private reapedLogPath;
    /**
     * The scope's consumed ids, cached by log size (see {@link reapedCache}). `null` = the log EXISTS
     * but could not be read: a state where this store cannot tell a fresh entryId from a spent one, and
     * the callers below take the fail-CLOSED arm rather than guess — refusing a boundary is a turn that
     * cannot be rewound to, while guessing wrong is post-turn bytes published under a pre-turn key.
     */
    private readReapedIds;
    /** Append ids to the scope's consumed-id ledger. Append-only and idempotent by construction (a
     *  membership test cannot tell a duplicated line from a single one), which is what lets a peer
     *  process write it without coordination — the reason this is a log and not a rewritten set. */
    private markReaped;
    private boundaryOrderOf;
    /** Publish a blob write-ONCE (EEXIST = the identical bytes are already stored — dedup no-op). */
    private putBlob;
    trackEdit(scope: string, absPath: string, env: ExecutionEnv, root: string, signal?: AbortSignal): Promise<FileHistoryTrackResult>;
    annulTrack(scope: string, absPath: string, root: string, verify?: {
        env: ExecutionEnv;
        signal?: AbortSignal;
    }): Promise<FileHistoryResult>;
    /**
     * Is this scope free of history RIGHT NOW? Synchronous on purpose (adversarial round 1): the
     * empty-target law is only sound if the observation and the publish happen with no await between
     * them — an emptiness result carried across an await is a claim about the past, and two concurrent
     * imports that both believed it would each delete the other's freshly published graph.
     */
    private scopeIsEmptySync;
    /**
     * The ONE-STEP publish shared by adoptScope and importHistory: verify emptiness and swap the
     * staged directory into place with NOTHING awaited in between, so the whole claim-and-publish is
     * atomic against any other in-flight publish in this process.
     *
     * THREE answers, not two (A-082 rescan): `"published"`, `"not_empty"` (the target stopped being
     * empty — the caller's typed `conflict`), and `"ledger_failed"` (the tombstones for the residue
     * this publish destroys could not be written). The second failure arrived with the tombstone write
     * below and rode the same `false` as the first, so both callers dressed an IO fault as the one
     * code the engine's fork-carry seat is contractually silent about — the exact silence the
     * `conflict`/`adopt_failed` split was introduced to close, re-opened one door over (and its message
     * asserted a fact that was not true: the target had gained nothing). They are separate answers now.
     */
    private publishStagedScopeSync;
    /** Committed boundary ids in a scope (a corrupt boundary is NOT listed — the listing agrees with
     *  canRestore/restore, so a half-readable directory never looks like more history than it serves). */
    private listBoundaryIds;
    snapshot(scope: string, entryId: string, env: ExecutionEnv, root: string, signal?: AbortSignal): Promise<FileHistoryResult>;
    restore(scope: string, entryId: string, env: ExecutionEnv, root: string, signal?: AbortSignal): Promise<FileHistoryRestoreResult>;
    canRestore(scope: string, entryId: string): Promise<boolean>;
    diffStats(scope: string, entryId: string, env: ExecutionEnv, root: string): Promise<FileHistoryDiffStats | null>;
    reap(scope: string, keepEntryIds: string[]): Promise<number>;
    /**
     * Blob GC across EVERY scope — FAIL-SAFE by construction: build the COMPLETE live set or keep
     * every blob. An incomplete live set (a swallowed scan error) would delete still-live bytes, so
     * any unexpected read aborts the pass and discloses (a leaked blob is harmless; a deleted live one
     * is data loss). Synchronous end to end, so it cannot interleave with a capture parked at an
     * await; the live set additionally unions the staging journals of THIS process's in-flight
     * attempts (bytes published but not yet chained), while every OTHER journal is dead-attempt
     * residue: it is removed here, and the blobs it was protecting become collectable in this pass.
     */
    private collectBlobs;
    adoptScope(fromScope: string, toScope: string): Promise<FileHistoryResult>;
    exportHistory(scope: string): Promise<FileHistoryExport | null>;
    importHistory(scope: string, data: FileHistoryExport, srcGetBlob: (hash: string) => Promise<Uint8Array | undefined>): Promise<FileHistoryResult>;
    /** The import's body, so EVERY exit (typed refusal, thrown fault, success) passes one `finally`
     *  that releases the journal + lease — an early return used to leak both, and a leaked lease is
     *  permanent liveness for blobs nothing references (adversarial round 2). */
    private runImport;
}
