Storage
A hav repo is one SQLite file, .hav/repo.db. Objects (file contents,
trees, conflicts) are content-addressed rows, and history, workspaces, and the
op journal are tables beside them. There is no object directory, no loose-file
store, and no maintenance daemon: one file to back up, one transaction per
command.
Ids never depend on storage
The invariant that everything below hangs from: an object's id is the BLAKE3
hash of its plaintext, always. Packing, chunking, and compression change how
bytes are stored. They never change what an id resolves to, and never a snap
or knot id. Fresh writes always land as whole loose rows, hav pack compacts
them after the fact, and reads resolve transparently across every layout.
hav pack: two regimes
hav pack compacts the store, choosing a strategy by size:
- Small objects (≤128 KiB), the common case of source files, batch into solid zstd packs, ordered so successive versions of the same file sit adjacent and compress against each other. A deeply-versioned text file stores compactly without per-file delta chains. (Delta reconstruction was the fragile part of every design that had it, and a solid compression window captures the same redundancy.)
- Large blobs (>128 KiB) are content-defined chunked (FastCDC). Each distinct chunk stores once, so versions of a big binary share everything but what actually changed. Big binaries are native, with no LFS sidecar.
pack is storage-only maintenance: it records no op, since nothing any view
references changes, and it runs on a workspace repo or a bare hub alike.
Packs never travel
Every egress (push, pull, clone, the harbor's bundle) expands packed objects
back to whole loose rows. The wire and the harbor's validation surface see
byte-for-byte whole objects regardless of how either side stores them. The
consequence: transfer is whole-history, and pack shrinks storage rather than
the wire. (Delta transfer is deferred to protocol v2.)
Sweep and pack
hav sweep is the space
reclaimer, dropping what nothing retained can reach, and pack is the space
compactor, storing what remains more densely. They compose: sweep dissolves
any pack holding a dropped member (survivors expand back to loose rows), and a
later pack re-compacts. The node_modules/ rescue is sweep-then-pack.
Format versioning
The database records the schema version that wrote it, and opening checks it before anything else:
- Same version: opens, and writes nothing. Reading a repo never rewrites its format marker.
- Older version: migrated forward in place. Upgrades are one-way; there is no downgrade path.
- Newer version: refused —
repo schema N is newer than this hav — upgrade hav. An unrecognized format means this binary would misread the repo, so it declines to touch it rather than proceed carefully.
Alongside the version, a repo can carry named extensions (none exist yet). An extension marks one capability a repo depends on, so a binary that lacks it refuses with the feature's name instead of a bare version number — and repos stay compatible with older binaries until they actually use something new.
The harbor applies the same rule to pushes: a bundle whose schema version doesn't match the hub's is rejected before a byte of it is applied, and the check reads the upload without modifying it.
Honest limits
- Sealed objects are neither chunked nor packed. They stay whole sealed rows. A large sealed binary gets no dedup, and a crew's sealed history gets no cross-version compression. The chunk-then-seal composition is deferred, and documented here rather than glossed over.
- Per-blob compressed sizes and chunk boundaries are observable to anyone with the database file. This is a coarse content-shape leak, the same class as the sealed-row metadata budget, where counts and sizes are visible and content is not.