/** Manifest schema version. An unknown version reads as "no manifest", which costs a re-upload of the * scope but never data — the caller treats the scope as unknown rather than guessing at its shape. */ export declare const WORKSPACE_MANIFEST_VERSION = 2; /** Byte target for one pack. Small enough that a repack is cheap and a single pack is a quick fetch; * large enough that hydrate is tens of objects rather than thousands. A file larger than this becomes * a pack of one, which is the correct degenerate case and needs no special branch. */ export declare const TARGET_PACK_BYTES: number; /** Repack when live bytes fall below this fraction of stored bytes. The one knob bounding dead weight; * below it a scope is paying to store bytes no live file references. */ export declare const REPACK_MIN_LIVE_FRACTION = 0.75; /** Repack when the pack count exceeds this multiple of the ideal count for the scope's size. Without * it, a long run appending a tiny pack per iteration would drift toward thousands of small packs and * quietly re-create the round-trip problem packs exist to remove. */ export declare const REPACK_MAX_PACK_COUNT_FACTOR = 3; /** Floor for the pack-count check, so a small scope is never repacked merely for having a few packs. */ export declare const REPACK_MIN_PACK_COUNT = 8; /** One persisted file. Short keys because this JSON is written on every persist. */ export interface WorkspaceFileEntry { /** Workspace-relative POSIX path. */ p: string; /** Size in bytes. Doubles as the file's length within its pack. */ s: number; /** mtime in ms. Restored on hydrate, so the next diff doesn't see every file as changed. */ m: number; /** Digest of the pack holding this file's bytes. */ k: string; /** Byte offset of this file within its pack's UNCOMPRESSED stream. */ o: number; } /** One stored pack. */ export interface WorkspacePackEntry { /** sha256 hex of the pack's STORED bytes — what the object is named and what a read verifies * against. Digesting the stored form (rather than the contents) is what makes verification exact: * a truncated or corrupted download fails the check without needing to be decompressed first. */ d: string; /** Stored size in bytes (post-compression). */ s: number; /** Uncompressed size in bytes — the denominator for this pack's live fraction. */ u: number; } export interface WorkspaceManifest { v: number; files: WorkspaceFileEntry[]; packs: WorkspacePackEntry[]; } /** A scope with nothing stored. Distinct from "no manifest": an empty manifest is a RESTORED state (the * scope was deliberately emptied) and must hydrate as empty, never as "fall back to something else". */ export declare function emptyManifest(): WorkspaceManifest; /** * What one run's local tree changed relative to its baseline. * * The separation of `baseline` from the store's CURRENT state is the whole point (§6). With no * concurrency they are identical. Under concurrency they diverge, and conflating them is what makes a * run emit deletes for a concurrent run's files: every path in the baseline that isn't on my disk looks * like something I deleted, when it may be something the other run created and I simply never had. */ export interface WorkspaceIntent { /** Files whose bytes must be written: new, or changed by size/mtime. No pack ref yet. */ write: readonly Omit[]; /** Files unchanged since the baseline — their bytes are already in a live pack, carried forward * as-is. This is what makes an unchanged file cost nothing, even during a repack decision. */ keep: readonly WorkspaceFileEntry[]; /** Paths present in the baseline and gone from disk: deletions this run genuinely made. */ delete: readonly string[]; } /** A local walk entry, before it is assigned to a pack. */ export type WorkspaceWalkEntry = Omit; /** * Diff the local tree against the baseline to produce this run's INTENT. * * Change detection is (size, mtime) — rsync's and incremental tar's default, with the same caveat: a * file rewritten to the same length whose mtime is then restored reads as unchanged. Hashing every byte * before every suspend point is the only alternative and reintroduces the cost packs exist to remove. * * A null baseline (first persist of this run, or an unreadable manifest) writes everything and deletes * nothing, which is correct: nothing of this scope is known to be ours yet, and inventing deletions * from an unknown baseline is how a run destroys state it never saw. */ export declare function diffIntent(local: readonly WorkspaceWalkEntry[], baseline: WorkspaceManifest | null): WorkspaceIntent; /** A path both this run and a concurrent one wrote. Resolved in favor of this run, and REPORTED — a * clobber that nobody is told about is the failure mode this system exists to eliminate. */ export interface WorkspaceConflict { path: string; /** What this run did to it. */ ours: "wrote" | "deleted"; } export interface WorkspaceMerge { /** The file set to store: this run's intent applied over the remote's current files. */ files: WorkspaceFileEntry[]; /** Paths where the remote moved under us since our baseline. */ conflicts: WorkspaceConflict[]; } /** * Apply an intent onto the store's CURRENT file set (§6's three-way merge). * * The guarantee: a path this run never touched is never disturbed, so a concurrent run's work survives * even though this run has no idea it happened. The non-guarantee, stated plainly in §6.1: per-file * resolution can interleave two runs into a set where every file is intact and the set is collectively * inconsistent. This function cannot fix that and should not pretend to. * * `remote === null` means the scope has nothing stored, so the intent stands alone. */ export declare function mergeIntent(intent: WorkspaceIntent, baseline: WorkspaceManifest | null, remote: WorkspaceManifest | null, written: ReadonlyMap): WorkspaceMerge; /** One pack to be built: which files go in it, at which offsets, in this order. */ export interface PackPlan { entries: { entry: WorkspaceWalkEntry; offset: number; }[]; /** Total uncompressed bytes this pack will hold. */ uncompressedBytes: number; } /** * Group the files that need bytes written into packs. * * Files are ordered by path first, which puts a directory's files adjacent, so files that tend to change * together tend to land in the same pack — that is what keeps the repack threshold from firing early on * a workload that edits one subtree. A file at or over the target becomes a pack of one; no branch is * needed for it, because closing a full pack before adding produces exactly that. */ export declare function planPacks(files: readonly WorkspaceWalkEntry[], targetBytes?: number): PackPlan[]; /** Digest naming a pack's stored bytes. Also the object's name, so a client can name a digest but never * a key — the scope prefix is always derived server-side. */ export declare function packDigest(storedBytes: Uint8Array): string; /** Packs referenced by `previous` and not by `next`: nothing live points into them, so they are deleted * in the same explicit step that already handles vanished paths. There is exactly ONE live manifest per * scope and every persist diffs previous against next, which is why this needs no reaper and can leak * nothing. */ export declare function unreferencedPacks(previous: WorkspaceManifest | null, next: WorkspaceManifest): string[]; /** * The pack entries `files` actually reference, carried over from the packs we know about. A pack no * file points into is dropped here, which is what makes {@link unreferencedPacks} see it as garbage. * * DEDUPED by digest, which is not cosmetic. The caller's `known` set is the remote's packs concatenated * with the ones just built, and those overlap whenever a rebuilt pack is byte-identical to a stored one * (the same content re-packed produces the same digest, so the upload is skipped but the entry is still * produced). Keeping both copies would inflate the manifest and, worse, double-count * `storedUncompressedBytes` in {@link decideRepack} — firing a full repack on a scope that has no dead * bytes at all. */ export declare function referencedPacks(files: readonly WorkspaceFileEntry[], known: readonly WorkspacePackEntry[]): WorkspacePackEntry[]; export interface RepackDecision { repack: boolean; /** Why, for the event stream — a repack is the expensive operation in this design, so when it fires * it must be visible rather than inferred from a latency graph. */ reason: "live_fraction" | "pack_count" | null; liveBytes: number; storedUncompressedBytes: number; packCount: number; } /** * Should the next persist rewrite every pack instead of appending? * * Two triggers, both bounding a way the append-only rule degrades: dead bytes accumulating inside packs * whose files have been superseded, and pack COUNT drifting up as a long run appends a small pack per * iteration. The second matters as much as the first — thousands of tiny packs would re-create the * round-trip cost that packs exist to remove, without ever tripping a live-fraction check. */ export declare function decideRepack(manifest: WorkspaceManifest): RepackDecision; /** * Parse a stored manifest. Returns null for anything unreadable or of an unknown version — the caller * then treats the scope as unknown, which costs a re-upload but never data. * * The manifest is ours, but it round-trips through storage, so nothing here trusts its shape. Path * SAFETY is enforced at restore time against the workspace root (a `..` in a stored path must not * escape), deliberately not here: this layer has no notion of where the workspace lives. */ export declare function parseWorkspaceManifest(bytes: Uint8Array): WorkspaceManifest | null; export declare function serializeWorkspaceManifest(manifest: WorkspaceManifest): Uint8Array;