import { Buffer } from "node:buffer"; import { type LockOptions } from "./lock.ts"; /** One parsed op-log line. `meta` is absent on a legacy bare-oid line. */ export interface OpLogEntry { oid: string; meta?: { lamport: number; line: string; workspace: string | null; }; } import type { AnyObject, ObjectType } from "../objects/types.ts"; /** * A stored object's bytes could not be decoded — bit-rot, truncation, or a torn write * that slipped past the atomic-write guarantee (D1). Typed (not an opaque `SyntaxError`) * and carries the offending `oid` so a decode failure deep in `materialize`/`pull` is * actionable: the caller knows exactly which object to `fsck`/repair. F1 (docs/13). */ export declare class CorruptObjectError extends Error { readonly oid?: string; constructor(message: string, opts?: { oid?: string; cause?: unknown; }); } export declare class ObjectStore { #private; readonly root: string; constructor(repoDir: string); init(): Promise; /** * Atomically write a derived auxiliary file under the repo's `.avcs` root, reusing the * crash-safe temp→fsync→rename→fsync-dir path (D1/D2). For repo-managed caches the * store doesn't model as objects — e.g. the compaction snapshot. `relPath` is resolved * under root; parent dirs are created. Crash-safe: a reader sees old-or-complete, and * the file survives a hard crash once this returns. */ writeAux(relPath: string, data: string | Buffer): Promise; /** Read an auxiliary file under the repo's `.avcs` root, or null when absent. The read * counterpart of {@link writeAux}: atomic writes guarantee old-or-complete, so a plain * read never observes a torn file. */ readAux(relPath: string): Promise; /** Durably append a line to an auxiliary log under the repo's `.avcs` root (e.g. the * hub audit log, E7). Reuses the fsync-file + fsync-dir append path. */ appendAux(relPath: string, line: string): Promise; /** Run a critical section under a named cross-process lock (see lock.ts). */ withLock(name: string, fn: () => Promise, opts?: LockOptions): Promise; appendEntityIndex(key: string, oid: string): Promise; readEntityIndex(key: string): Promise; /** * Where `repoDir`'s store physically lives. Normally `/.avcs`. But `.avcs` may * instead be a *pointer file* holding a single `avcsdir: ` line, in which case the * store lives at that path and several working trees share ONE store — the same trick git * uses when a linked working tree's `.git` is a file saying `gitdir: `. * * Deliberately git-free: the pointer is AVCS's own marker, so this works for any kind of * linked working tree, or for none at all. A relative path is resolved against the * directory holding the pointer. Pure/synchronous — every store open goes through it. * A malformed or unreadable pointer degrades to the plain path, so the caller's usual * "not an AVCS repo" error surfaces instead of a parse throw. */ static resolveStoreDir(repoDir: string): string; static isRepo(repoDir: string): boolean; /** * Locate the AVCS repo that owns `startDir` by walking up the directory tree until a * directory satisfies {@link isRepo} (i.e. has `.avcs/objects`). This is AVCS's own * root-finding — analogous to how git ascends to find `.git`, but keyed entirely on * AVCS's own marker so it works with no git present. Returns the owning repo dir, or * `null` if no ancestor (including `startDir` itself) is a repo. Pure/synchronous so * the CLI and MCP server can both resolve a working dir to its store cheaply. */ static findRepoRoot(startDir: string): string | null; /** * Write an object. Returns its oid. Idempotent: writing identical content yields * the same oid and is a no-op if already present (append-only honored). */ put(obj: T): Promise; /** * Op-log in authoring/arrival order (docs/11 A5). Returns oids of every operation ever * written, deduped, FIRST-WRITE order preserved. May include oids of operations later * removed by GC (the store is the source of truth — callers tolerate a missing object). * Empty for a store created before the op-log existed; `rebuildOpLog` backfills it. */ readOpLog(): Promise; /** * The same log, with whatever per-operation metadata its lines carry. * * A line is either a bare oid (the original format) or a TAB-separated record: * * \t\t\t * * Those are exactly the fields a reader needs to decide whether an operation belongs in a * view WITHOUT reading its body — `materialize` filters by line and workspace, and the * Lamport reseed wants the highest lamport. Recovering three small fields used to mean * reading and decoding every operation object in the store. * * Old lines stay valid, so no store needs converting: an entry with no record reports no * `meta` and the caller falls back to reading the object. And because the log is deduped * BY OID with the FIRST occurrence fixing order, a record for an oid already present as a * bare line can simply be APPENDED — position comes from the old line, metadata from the * new one. Upgrading is therefore append-only: it never rewrites history, and it cannot * lose a concurrent append the way a read-modify-write rebuild could. * * A line with MORE than four fields is read as a record and the extras ignored, so a store * written by a later version stays readable here. */ readOpLogEntries(): Promise; /** One op-log line for a newly written operation — a record when we can build one. */ static opLogLine(oid: string, op: unknown): string; /** * Append records for operations whose log lines carry no metadata (best-effort upgrade). * * Append-only by design — see `readOpLogEntries`. Returns how many records were written. */ appendOpLogRecords(ops: readonly unknown[]): Promise; /** * Object-log in arrival order (E5): oids of EVERY object ever written, deduped, * first-write order preserved. A store predating the log (or one that just upgraded) * is backfilled once from a full scan — that scan order becomes this hub's stable * append base. Append-only afterward, so an index into it is a valid sync cursor. */ readObjLog(): Promise; /** * Backfill the op-log from a full scan (for stores predating it, or after corruption). * Rewrites it atomically to the current operation set in canonical oid order. * * The order is oid order, NOT authoring order: a shard scan cannot recover the order the * operations were written in. That is a real weakening of `readOpLog`'s first-write-order * contract, which is why this runs only where the log is absent or already untrustworthy — * the append-only upgrade path (`appendOpLogRecords`) is what an existing, intact log uses. * Determinism does not rest on it either way: the reducer tie-breaks by `(lamport, oid)`. */ rebuildOpLog(): Promise; /** * Redaction exception (Phase 12): overwrite the object stored AT `oid` with new * content that no longer hashes to it. This is the ONE place append-only/content- * addressing yields — used only by an admin-signed Redaction to evict leaked bytes * while keeping the oid (and every reference to it) valid. */ overwriteAt(oid: string, obj: AnyObject): Promise; /** * GC exception: delete an object file. Used only by `repo.gc` to reclaim objects * that are unreachable from the authoritative graph (orphan blobs, expired * quarantined ops). The append-only audit history of accepted ops is never removed. */ deleteObject(oid: string): Promise; get(oid: string): Promise; has(oid: string): Promise; /** * Run `fn` with every `put`/`appendEntityIndex` STAGED in memory, then flush the lot as * one group commit (issue #33 / the third site of #55's perf finding). * * The authoring path — `commitWorkingTree` looping blob put + op put + index append per * file — paid the same serial-fsync amplification the transfer paths did: a 100-file * commit measured 4.69s on an idle machine, and the avcs hook stages the WHOLE worktree, * which is how pre-commit ingest reaches 30s under load (#33). * * Semantics: * - read-your-writes: `has`/`get` serve staged objects, so authoring code that reads * what it just wrote keeps working. * - durability AT RETURN is unchanged — the flush is `putMany`'s group commit, so the * oplog is appended only after every body is durable, exactly as `put` promises. * - a throw inside `fn` leaves NOTHING on disk: a crash mid-commit becomes a clean * no-op instead of a partial commit — strictly better than the sequential behavior. * - lamport quality is unaffected: the in-process clock ticks per staged op, and * `#maxLamportSeen` is a cross-process ordering QUALITY aid whose absence during the * batch changes nothing the reducer depends on (it tie-breaks by (lamport, oid)). * - contention checks inside the batch see pre-batch state only; same-batch ops share * one actor, which the check never warns about anyway. * * Nesting is refused rather than flattened — a silently flattened inner batch would make * the outer one's "all or nothing" a lie. */ batched(fn: () => Promise): Promise; /** * Store many objects with GROUP-COMMITTED durability (issue #55's perf follow-up). * * `put` costs 4 serial fsyncs per object (tmp fsync + dir fsync + objlog append's file and * dir fsyncs; +2 more for an operation's oplog entry). On macOS each is milliseconds, so a * transfer paid ~25ms/object — a 1,602-object push took 145s with a 98.4%-idle CPU profile. * The bytes were never the cost; the round trips were. * * This keeps every durability guarantee and reorders the waiting: * * 1. bodies: tmp-write + fsync with bounded parallelism, then rename, then ONE dir * fsync per distinct shard touched — same "old file or complete new file" atomicity. * 2. oplog: ONE append for the chunk's operations. Order is the contract: the reducer * trusts every oplog line to resolve, so the append happens only after every body in * the chunk is durable — the same "AFTER the object is durable" rule `put` documents. * 3. objlog: ONE append for everything new. * * Work is chunked (128) so a crash exposes at most one chunk's window — the same failure * class as a sequential loop dying between an object's rename and its log append, just * bounded instead of per-object. Both logs are rebuildable caches. * * Content addressing is unchanged: the incoming `oid` field is ignored and recomputed, so * a forged object lands at its own address here exactly as it does in `put`. */ putMany(objects: AnyObject[]): Promise<{ oid: string; existed: boolean; }[]>; /** * Append many entity-index entries with one durable append PER KEY FILE instead of per * entry. A pull indexes every arriving operation, which was 2 more fsyncs each; grouping * by key keeps the per-key ORDER (blame reads it) while a 1,600-op pull touching 25 files * pays 25 appends instead of 1,600. */ appendEntityIndexMany(entries: [key: string, oid: string][]): Promise; listOids(type?: ObjectType): Promise; list(type?: ObjectType): AsyncGenerator; /** * Fold all loose NON-blob objects into a new packfile (+ index), then delete the loose * copies. Idempotent in effect (already-packed objects have no loose file). Reads stay * correct throughout (loose-first, then packs). Returns how many objects were packed. */ pack(): Promise<{ packed: number; }>; /** * Every object of a type, as an array. * * Reads in a bounded fan-out rather than one at a time. This used to drain `list()` — a * generator that awaits one `readFile` per object — so gathering N objects cost N * round-trips of latency with the CPU idle between them, and every caller that wants a * whole type paid it: `gc` (operations, then blobs), evidence/decision gathering, intent * and lease listing, the MCP context and watch paths. * * `listOids` walks the same shards in the same order without reading a body, so the oids * are known up front and the reads are independent. Order and set are therefore identical * to `list()`'s — several callers index or diff the result — and a corrupt body still * throws rather than shortening the answer. * * `list` stays a generator on purpose: it is the memory-bounded streaming API, and a * caller that streams does not want the whole type buffered. */ collect(type?: ObjectType): Promise; /** * How many object reads a single `collect` keeps in flight. * * High enough that latency stops dominating, low enough to stay well inside a default * file-descriptor limit while other work also has files open. */ static readonly READ_CONCURRENCY = 64; setRef(name: string, oid: string): Promise; getRef(name: string): Promise; /** All named refs as name → oid (for hub governance distribution). */ listRefs(): Promise>; setHead(viewName: string): Promise; getHead(): Promise; /** * Re-derive every stored object's content address and compare it to the address it * lives at — catching bit-rot, truncation, and a torn write that slipped past the * atomic-write guarantee. A redacted blob is exempt: its bytes were deliberately * overwritten by an admin Redaction and no longer hash to their oid (that's the one * sanctioned exception to content-addressing). Also reconciles the op-log against the * actual operation set: operation objects missing from the log are real drift (the * fast-path could skip them); log entries with no object are GC'd/lost (informational). * Read-only unless `rebuild` is set, which rewrites the op-log to match the object set. */ fsck(opts?: { rebuild?: boolean; }): Promise; } export interface FsckReport { objectsChecked: number; /** true iff no corrupt object and no operation missing from the op-log. */ ok: boolean; /** Objects whose content no longer hashes to the address they live at. */ corrupt: { oid: string; reason: string; }[]; oplogDrift: { /** operation objects absent from the op-log — real drift (fast-path could skip them). */ opsMissingFromLog: string[]; /** op-log entries with no backing object — GC'd quarantine ops or lost (informational). */ logEntriesMissingObject: string[]; }; /** Present when `fsck({rebuild:true})` repaired op-log drift. */ repaired?: { oplogRebuilt: boolean; oplogEntries: number; }; } //# sourceMappingURL=objectStore.d.ts.map