Status: proposed v2 contract, intended to be practical to implement and stable enough to freeze.
Many programs need to fetch, store, and re-read opaque byte payloads from external sources — web pages, API responses, regulatory documents, configuration snapshots, binary artifacts, and dataset shards. The common requirements are:
- Do not re-store exact duplicates. Identical raw bytes should collapse to one blob.
- Preserve observation history. A locator going
A → B → Ais three distinct historical runs, not one merged record. - Support cheap hot queries. Latest-by-locator, point-in-time resolution, exact read-by-digest, and freshness checks should stay boring.
- Keep storage small. Repetitive corpora should benefit from zstd, trained dictionaries, locator-local deltas, and large-blob chunk deduplication.
- Stay local and queryable. One SQLite file, no daemon, no server, and SQL-friendly layout.
Farchive is a positive-observation archive for opaque bytes. It records what was observed at a locator and when. It does not model first-class tombstones, negative existence, or inferred world state between observations.
Farchive provides:
- exact raw-byte storage
- content identity by SHA-256 over raw bytes
- locator-scoped span history
- optional append-only event audit
- transparent storage optimization under a stable semantic model
Its design separates:
- semantics: blobs, locators, state spans, observation time
- physical representation: raw frames, zstd, zstd with dictionaries, locator-local deltas, and chunked large-blob storage
Compression is subordinate to correctness. No storage optimization may change user-visible bytes, digests, span history, or resolution results.
A blob is an immutable raw byte string with content-derived identity.
A digest is the archive identifier for a blob. The official profile uses bare SHA-256 hex:
<64 lowercase hex chars>
Digest identity is computed over the raw uncompressed bytes.
A locator is an opaque external key naming where bytes were observed.
Examples: HTTP URLs, S3 keys, filesystem paths, application-specific keys.
The archive does not interpret locator structure.
A state span is one contiguous locator-scoped run during which the archive’s best-known state for a locator was one blob.
A span has:
observed_from— inclusive lower boundobserved_until— exclusive upper bound, orNULLif currentlast_confirmed_at— latest confirming observationobservation_countlast_metadata— optional caller metadata snapshotseries_key— optional opaque lineage hint used only for delta candidate selection
Example:
- locator observed as
Aatt1 - observed as
Aagain att2 - observed as
Batt3 - observed as
Aagain att4
This yields three spans:
Afromt1untilt3, last confirmed att2Bfromt3untilt4, last confirmed att3Afromt4until open-ended, last confirmed att4
An event is an append-only audit record of an archive operation.
Event logging is an archive property, not a session property. Once any session creates the event table (via enable_events=True), all later sessions append and can read events automatically.
The official v2 profile emits these kinds:
| Kind | Emitted by | Scope |
|---|---|---|
fa.observe |
successful observation writes | locator-scoped |
fa.store |
store() |
locator-scoped |
fa.store_batch |
store_batch() |
archive-wide |
fa.train_dict |
train_dict() |
archive-wide |
fa.repack |
repack() when at least one blob is rewritten |
archive-wide |
fa.rechunk |
rechunk() when at least one blob is rewritten |
archive-wide |
For locator-scoped events (fa.observe, fa.store), locator is the affected locator and digest is the affected blob digest.
For archive-wide maintenance events (fa.store_batch, fa.train_dict, fa.repack, fa.rechunk), locator is the empty string and digest is None unless some future operation gives them a more specific meaning.
Event.metadata is informative and semi-open. Event kind strings and emission conditions are stable; metadata keys are not frozen.
A storage class is a caller-provided compression hint such as xml, html, pdf, text, binary, json.
Storage class is not normative MIME truth. It is an optimization bucket used for dictionary training and similar policy decisions.
series_key is an optional caller-provided optimization hint for delta candidate grouping.
- It does not define locator semantics.
- It has no effect on reads, span identity, or resolver behavior.
- It is advisory and may be absent.
- It can be attached through
store(..., series_key=...),observe(..., series_key=...), andBatchItem.series_key. - It is advisory and must be treated as a hint, not a correctness requirement.
- It is additive: no profile object, no write-path policy switching.
- It is persisted on resulting spans when provided.
- For an open same-digest span, the official v2 behavior is latest-non-null wins:
providing a different non-null
series_keyon a later confirmation updates the stored span value in place.
Farchive keeps caller metadata schema-agnostic, but these keys are recommended for import/provenance metadata when available:
source_urlsource_surfaceentry_namefetched_atimport_runartifact_roleupstream_etagupstream_last_modifiedupstream_version_id
Notes:
- These keys are advisory, not required or enforced.
- Prefer ISO 8601 UTC strings for timestamps.
- Prefer
artifact_roleover storage-oriented names such asstorage_reason.
An inline representation is a blob stored directly in the blob.payload column, with one of these codecs:
rawzstdzstd_dictzstd_delta
A delta representation stores a blob as zstd_delta against one older base blob from the same locator, or from another locator in the same non-null series_key lineage.
Delta is:
- locator-local by default; optionally supplemented by same-
series_keycandidates - depth-1 only
- optional and policy-driven
- invisible to readers
At selection time, delta bases are drawn only from inline non-delta blobs (raw, zstd, zstd_dict).
After later maintenance, a blob serving as the historical base of an existing delta may be physically rewritten to another non-delta representation such as chunked, as long as its raw bytes remain readable. Delta decompression depends on the base blob’s raw bytes, not on its original inline storage form.
A chunked representation stores a large blob as an ordered manifest of content-defined chunks.
Chunks are stored in a separate chunk table and deduplicated archive-wide by their own SHA-256 digest. A chunked blob has:
- one row in
blobwithcodec='chunked' payload=NULLstored_self_size=0- one or more rows in
blob_chunk
Chunking is an explicit maintenance optimization in v2. It is applied by rechunk(), not by store() or put_blob().
A chunk is an immutable raw byte substring identified by SHA-256 of its raw bytes and stored independently in the chunk table.
Blob identity MUST be computed over raw uncompressed bytes.
Once a digest exists, the raw bytes associated with that digest MUST NOT change.
Physical representation changes MUST NOT change:
- digest
- raw bytes
- locator history
- point-in-time resolution
- event history already written
Two non-contiguous runs of the same digest at one locator MUST remain two spans.
At any instant, a locator has at most one open current span.
If raw bytes already exist in the archive, writing the same bytes again reuses the existing blob digest. Exact dedup happens before any delta decision.
A delta blob MUST NOT reference another delta blob as its base.
Chunking is only a storage optimization. A chunked blob is still one logical blob with one digest and one raw byte string.
Once written, events are not rewritten or deleted by normal archive operations.
train_dict(), repack(), and rechunk() may change physical storage but MUST preserve raw bytes, digests, spans, and query results.
Every observation has an observed_at instant in UTC, stored as Unix milliseconds (INTEGER) in the official profile.
Observations for a given locator MUST be appended in nondecreasing observed_at order.
Out-of-order writes are rejected.
A digest transition at exactly the current span’s last_confirmed_at is rejected to prevent zero-duration spans.
Same-timestamp confirmations of the same digest are allowed.
A span is current iff observed_until IS NULL.
A span is active at instant t iff:
observed_from <= t- and either
observed_until IS NULLort < observed_until
For calls without explicit observed_at, implementations may normalize generated times to keep span history valid:
- same-digest confirmation: bump to at least current
last_confirmed_at - digest transition: bump to at least current
last_confirmed_at + 1
This normalization affects the effective observation time and therefore the fa.observe and fa.store event timestamps for that write.
The official v2 profile supports these blob codecs:
rawzstdzstd_dictzstd_deltachunked
payload contains exact raw bytes.
payload contains vanilla zstd-compressed bytes.
payload contains zstd-compressed bytes using a trained dictionary referenced by codec_dict_id.
payload contains zstd-compressed bytes using prefix-delta mode against base_digest.
base_digest references another blob whose raw bytes are used as the prefix base at decompression time.
payload is NULL. Raw bytes are reconstructed by concatenating chunk payloads in blob_chunk.ordinal order after decoding each referenced chunk.
blob_chunk.raw_offset is preserved as manifest metadata and integrity aid, but reconstruction order is authoritative by ordinal in the official v2 profile.
For a blob row, stored_self_size means:
- inline codecs: physical size of the inline payload stored on that blob row
- chunked:
0
Chunk payload bytes live in chunk.stored_size, not in blob.stored_self_size.
ArchiveStats.total_stored_bytes SHOULD count archive-owned physical bytes:
- sum of
blob.stored_self_size - plus sum of unique chunk payload bytes in
chunk - plus dictionary bytes in
dict
This is a whole-archive metric, not a per-blob attribution metric.
Because chunked blobs store stored_self_size=0, codec-level reporting for chunked blobs MAY expose an auxiliary informative metric such as logical_stored, representing archive-wide unique chunk bytes referenced by chunked blobs.
This metric is informative only and is not a per-blob cost attribution.
ArchiveStats.db_file_bytes is an informative live-footprint metric. It MAY include:
- main database file bytes
- WAL sidecar bytes
- SHM sidecar bytes
if such sidecars exist at measurement time.
CompressionPolicy is a policy object, not an archive semantic identity.
@dataclass
class CompressionPolicy:
raw_threshold: int = 64
auto_train_thresholds: dict[str, int] = {"xml": 1000, "html": 500, "pdf": 16}
dict_target_sizes: dict[str, int] = {"xml": 112*1024, "html": 112*1024, "pdf": 64*1024}
compression_level: int = 3
delta_enabled: bool = True
delta_min_size: int = 4 * 1024
delta_max_size: int = 8 * 1024 * 1024
delta_candidate_count: int = 4
delta_size_ratio_min: float = 0.5
delta_size_ratio_max: float = 2.0
delta_min_gain_ratio: float = 0.95
delta_min_gain_bytes: int = 128
chunk_enabled: bool = True
chunk_min_blob_size: int = 1 * 1024 * 1024
chunk_avg_size: int = 256 * 1024
chunk_min_size: int = 64 * 1024
chunk_max_size: int = 1 * 1024 * 1024
chunk_min_gain_ratio: float = 0.95
chunk_min_gain_bytes: int = 4096The official profile uses the above defaults.
@dataclass(frozen=True, slots=True)
class StateSpan:
span_id: int
locator: str
digest: str
observed_from: int
observed_until: int | None
last_confirmed_at: int
observation_count: int
series_key: str | None = None
last_metadata: dict[str, Any] | None = None
@dataclass(frozen=True, slots=True)
class Event:
event_id: int
occurred_at: int
locator: str
digest: str | None
kind: str
metadata: dict[str, Any] | None
@dataclass
class CompressionPolicy:
...
@dataclass
class ImportStats:
items_scanned: int = 0
items_stored: int = 0
items_deduped: int = 0
bytes_raw: int = 0
bytes_stored: int = 0
@dataclass
class BatchItem:
locator: str
data: bytes
observed_at: datetime | None = None
storage_class: str | None = None
series_key: str | None = None
metadata: dict[str, Any] | None = None
@dataclass
class RepackStats:
blobs_repacked: int = 0
bytes_saved: int = 0
@dataclass
class RechunkStats:
blobs_rewritten: int = 0
chunks_added: int = 0
bytes_saved: int = 0
@dataclass(frozen=True, slots=True)
class LocatorHeadComparison:
locator: str
current_span: StateSpan | None
candidate_digest: str
status: Literal["absent", "same", "changed"]
@dataclass
class PurgeStats:
locators_requested: int = 0
locators_purged: int = 0
spans_deleted: int = 0
blobs_deleted: int = 0
chunks_deleted: int = 0
dry_run: bool = False
@dataclass(frozen=True, slots=True)
class ArchiveStats:
locator_count: int
blob_count: int
span_count: int
dict_count: int
total_raw_bytes: int
total_stored_bytes: int
compression_ratio: float | None
codec_distribution: dict[str, dict]
db_path: str
schema_version: int
chunk_count: int
db_file_bytes: intcodec_distribution is informative. Implementations SHOULD expose at least count, raw, and stored-style counters, and MAY add codec-specific keys such as logical_stored.
Creates or opens an archive.
db_path: SQLite file pathcompression:CompressionPolicy | Noneenable_events: creates the event table if absent
Context manager use (with Farchive(...) as fa:) closes the connection automatically.
Stores a blob if absent and returns its digest.
Semantics:
- exact dedup only
- may use trained dictionaries
- does not use delta (no locator context)
- does not use chunking in v2
- may trigger best-effort auto-training after a successful semantic write
Records an observation of an existing digest at a locator.
Cases:
- no current span → create new span
- same digest → extend current span
- different digest → close old span and create new span
- optional
series_keycan be persisted on the new/active span when provided - when extending an open same-digest span, a non-null provided
series_keyreplaces the current storedseries_keyvalue
8.4 store(locator, data, *, observed_at=None, storage_class=None, metadata=None, series_key=None) -> str
Atomic put_blob(data) + observe(locator, digest).
Semantics:
- exact dedup first
- may use trained dictionaries
- may use locator-local delta if beneficial
- may use same-
series_keycross-locator delta candidates ifseries_keyis provided - does not use chunking automatically in v2
- emits
fa.observeandfa.storewhen event history exists
8.5 store_batch(items, *, observed_at=None, storage_class=None, progress=None, series_key=None) -> ImportStats
Stores (locator, data) or BatchItem items in one atomic batch.
Semantics:
- all-or-nothing for the batch
- accepts both
list[tuple[str, bytes]](legacy) andlist[BatchItem] - per-item
observed_at,storage_class,series_key, andmetadataoverride shared defaults - each item still follows exact dedup and optional delta rules
- items later in the same batch may observe state written earlier in the same batch
- may emit many
fa.observeevents and onefa.store_batchsummary event
progress(current, total) MAY be called every 1000 scanned items.
Returns exact raw bytes for a digest, regardless of physical representation.
Compares candidate content to the locator head.
Rules:
- exactly one of
dataordigestmust be provided - status is one of
absent,same, orchanged
Returns:
- current span if
at is None - unique active span at time
atotherwise
Convenience: resolve() followed by read().
All spans for a locator, newest first.
StateSpan carries series_key when present.
Machine-readable outputs include series_key when present for:
resolve --jsonhistory --jsonls spans --json(farchive ls spans ... --json)
Returns True iff locator has a current span and, when a finite freshness window is supplied, that span is fresh enough.
Returns distinct locators matching a SQL LIKE pattern, sorted lexicographically.
8.13 events(locator=None, *, since=None, kind=None, digest=None, locator_prefix=None, limit=1000) -> list[Event]
Returns newest-first events filtered by locator and/or lower time bound.
Optional kind, digest, and locator_prefix filters are also supported.
Returns empty list if the archive has no event table.
Trains a zstd dictionary from sampled raw bytes in one storage class and returns dict_id.
Semantics:
- storage-class scoped only
- new blobs of that class use it immediately
- existing blobs are unchanged until
repack() - sampling operates over raw bytes, regardless of whether the sampled blobs are inline, delta, or chunked
8.15 repack(*, dict_id=None, storage_class=None, series_key=None, dict_group=None, batch_size=1000) -> RepackStats
Recompresses eligible vanilla-zstd inline blobs to zstd_dict.
Semantics:
- targets only blobs with
codec='zstd'and no dictionary - does not touch
raw,zstd_delta, orchunked - optional cohort filters narrow the candidate set before rewrite:
storage_class(existing semantics)series_key(blob must be referenced by at least one locator span with thatseries_key)dict_groupis an optional future axis only if implemented
batch_sizecaps successful repacks, not rows examined- one call is atomic
8.16 rechunk(*, storage_class=None, series_key=None, dict_group=None, batch_size=100, min_blob_size=None) -> RechunkStats
Converts eligible inline or delta blobs to chunked representation when beneficial.
Semantics:
- explicit maintenance operation
- requires chunking capability (
pyfastcdcor equivalent) - respects
chunk_enabled - optional cohort filters narrow the candidate set before rewrite:
storage_class(existing semantics)series_key(blob must be referenced by at least one locator span with thatseries_key)dict_groupis an optional future axis only if implemented
- candidates are non-chunked blobs with
raw_size >= min_blob_size - may rewrite
raw,zstd,zstd_dict, orzstd_deltablobs batch_sizecaps blobs rewritten per call- each blob rewrite is atomic; the whole call is not required to be all-or-nothing
- emits one
fa.rechunkevent if at least one blob is rewritten
Removes all spans for the listed locators and deletes unreferenced physical payloads.
Semantics:
- all-or-nothing for the call
- all input locators are deduplicated before processing
- removes locator spans for listed locators
- computes kept blobs as digests still referenced by remaining locators
- keeps any blobs reachable through
base_digestchains from kept blobs - deletes all non-kept blobs and any unreferenced chunks when not in dry-run mode
- emits a
fa.purgeevent when not dry-run and at least one locator was purged
Returns an informative archive snapshot.
Closes the database connection. Using the instance afterward is invalid except via reopening a new instance.
Span metadata MUST be a JSON object (dict[str, Any] where values are JSON-serializable) or None.
On same-digest confirmation:
metadata={"k": "v"}replaceslast_metadatametadata=Nonepreserves existing metadata{}is a valid empty metadata object
Event.metadata is semi-open and informative. It is not part of the frozen semantic core.
Delta encoding is only considered by store() and store_batch(), because both know the locator being updated.
Candidate bases are drawn from recent unique digests previously observed at:
- the same locator, and
- when
series_keyis provided, other locators that share thatseries_key, with the same general policy filters.
When both sources are available, locator-local candidates are always considered alongside
series_key candidates using the same filters; exact ordering is implementation-defined and affects only performance.
- count cap:
delta_candidate_count - raw size lower bound:
delta_min_size - raw size ratio bounds:
delta_size_ratio_min/delta_size_ratio_max
At selection time, a base blob MUST be inline and non-delta (raw, zstd, or zstd_dict).
Chunked blobs are not selected as delta bases in the official v2 profile.
Delta is used only when it beats the best inline frame by both thresholds:
- relative gain:
delta_min_gain_ratio - absolute gain:
delta_min_gain_bytes
Thresholds are evaluated against the best inline frame, not against other delta candidates.
Delta depth is exactly one. Implementations MUST NOT build chains of deltas.
If the new raw bytes already exist as a digest, that exact digest is reused and no delta blob is created.
Chunking is a maintenance transformation in v2. store() and put_blob() do not produce chunked blobs automatically.
The official profile uses FastCDC-style content-defined chunking. If the chunking implementation is unavailable, all non-chunking archive operations still work, but rechunk() MUST fail cleanly.
rechunk() rewrites a blob only if chunking reduces incremental archive-owned bytes enough to beat both thresholds:
chunk_min_gain_ratiochunk_min_gain_bytes
The comparison baseline is the blob’s current inline stored_self_size.
Each chunked blob has ordered blob_chunk rows keyed by:
blob_digestordinalraw_offsetchunk_digest
ordinal is authoritative for reconstruction order in the official v2 profile.
A blob may reference the same chunk digest multiple times at different ordinals and offsets.
Readers SHOULD validate that a chunked manifest is structurally sane. At minimum:
- at least one chunk row exists
- ordinals are contiguous from
0 - reconstructed byte length matches
blob.raw_size
Chunks are deduplicated across the whole archive, not per locator or per storage class.
One event per successful observation write. occurred_at is the effective observation time after any implicit timestamp normalization.
One event per successful store() call. For a given store, its fa.store.occurred_at SHOULD equal the corresponding fa.observe.occurred_at.
One summary event per successful batch call.
One event per successful dictionary training.
One event per successful repack call that rewrites at least one blob.
One event per successful rechunk call that rewrites at least one blob.
One event per successful purge call that deletes locator spans.
The following behaviors are part of the public contract:
| Condition | Exception | Message pattern |
|---|---|---|
observe(locator, missing_digest) |
ValueError |
not found — call put_blob() first |
| Out-of-order observation | ValueError |
Out-of-order observation |
| Same-timestamp digest change | ValueError |
Same-timestamp digest change |
| Non-dict metadata | TypeError |
metadata must be a dict or None |
| Non-JSON-serializable metadata | TypeError |
metadata must be JSON-serializable |
repack() without scoping |
ValueError |
requires storage_class or dict_id |
repack() with mismatched dict/class |
ValueError |
does not match |
repack() with unknown dict_id |
ValueError |
dict_id ... not found |
train_dict() without storage class |
ValueError |
requires storage_class |
train_dict() with too few samples |
ValueError |
Need at least 10 samples |
purge() with no locators |
ValueError |
At least one locator is required for purge(). |
rechunk() without chunking capability |
ValueError |
requires pyfastcdc or equivalent |
rechunk() when chunking policy disabled |
ValueError |
chunking not enabled |
| DB version newer than library | RuntimeError |
Upgrade farchive |
Corruption of delta or chunk manifests is not considered a normal caller error. Implementations MAY raise ValueError, RuntimeError, or codec-specific exceptions on corrupted archives.
Atomic with respect to:
- blob insertion if needed
- span update/insert
fa.observefa.store
Atomic with respect to:
- span update/insert
fa.observe
All-or-nothing for the whole batch.
Atomic per call.
Atomic per call.
Atomic per rewritten blob. A call may partially complete across multiple blobs and return aggregate stats for the successful rewrites.
- SQLite WAL mode
busy_timeout = 5000foreign_keys = ON- default transaction mode for atomic
with conn:blocks - POSIX advisory file lock for single-writer coordination
- no thread sharing of one
sqlite3connection
CREATE TABLE schema_info (
version INTEGER NOT NULL,
created_at INTEGER NOT NULL,
migrated_at INTEGER,
generator TEXT
);
CREATE TABLE dict (
dict_id INTEGER PRIMARY KEY,
storage_class TEXT NOT NULL DEFAULT '',
trained_at INTEGER NOT NULL,
sample_count INTEGER NOT NULL,
dict_bytes BLOB NOT NULL,
dict_size INTEGER NOT NULL
);
CREATE TABLE blob (
digest TEXT PRIMARY KEY,
payload BLOB,
raw_size INTEGER NOT NULL,
stored_self_size INTEGER NOT NULL,
codec TEXT NOT NULL CHECK (
codec IN ('raw', 'zstd', 'zstd_dict', 'zstd_delta', 'chunked')
),
codec_dict_id INTEGER REFERENCES dict(dict_id),
base_digest TEXT REFERENCES blob(digest),
storage_class TEXT,
created_at INTEGER NOT NULL,
CHECK (
(codec = 'chunked' AND payload IS NULL)
OR
(codec <> 'chunked' AND payload IS NOT NULL)
),
CHECK (
(codec = 'zstd_dict' AND codec_dict_id IS NOT NULL)
OR
(codec <> 'zstd_dict')
),
CHECK (
(codec = 'zstd_delta' AND base_digest IS NOT NULL)
OR
(codec <> 'zstd_delta' AND base_digest IS NULL)
)
);
CREATE TABLE chunk (
chunk_digest TEXT PRIMARY KEY,
payload BLOB NOT NULL,
raw_size INTEGER NOT NULL,
stored_size INTEGER NOT NULL,
codec TEXT NOT NULL CHECK (
codec IN ('raw', 'zstd', 'zstd_dict')
),
codec_dict_id INTEGER REFERENCES dict(dict_id),
created_at INTEGER NOT NULL,
CHECK (
(codec = 'zstd_dict' AND codec_dict_id IS NOT NULL)
OR
(codec <> 'zstd_dict')
)
);
CREATE TABLE blob_chunk (
blob_digest TEXT NOT NULL REFERENCES blob(digest),
ordinal INTEGER NOT NULL,
raw_offset INTEGER NOT NULL,
chunk_digest TEXT NOT NULL REFERENCES chunk(chunk_digest),
PRIMARY KEY (blob_digest, ordinal)
);
CREATE TABLE locator_span (
span_id INTEGER PRIMARY KEY,
locator TEXT NOT NULL,
digest TEXT NOT NULL REFERENCES blob(digest),
observed_from INTEGER NOT NULL,
observed_until INTEGER,
last_confirmed_at INTEGER NOT NULL,
observation_count INTEGER NOT NULL DEFAULT 1,
last_metadata_json TEXT
);
CREATE UNIQUE INDEX idx_span_one_open
ON locator_span(locator) WHERE observed_until IS NULL;
CREATE INDEX idx_span_locator
ON locator_span(locator, observed_from DESC);
CREATE INDEX idx_span_locator_time
ON locator_span(locator, observed_from, observed_until);
CREATE INDEX idx_blob_base
ON blob(base_digest);
CREATE INDEX idx_blob_chunk_ref
ON blob_chunk(chunk_digest);
-- optional
CREATE TABLE event (
event_id INTEGER PRIMARY KEY,
occurred_at INTEGER NOT NULL,
locator TEXT NOT NULL,
digest TEXT,
kind TEXT NOT NULL,
metadata_json TEXT
);
CREATE INDEX idx_event_locator_time
ON event(locator, occurred_at DESC);Chunk rows are shared across blobs. A chunk row’s meaning is its raw bytes, not its physical payload encoding.
- inline blob codecs use
payload - chunked blobs use
payload=NULL stored_self_sizeexcludes chunk bytesbase_digestis only valid forzstd_delta
- schema 1: legacy profile without delta or chunking
- schema 2: legacy profile with delta, without chunking
- schema 3: v2 profile with delta and chunking support
A v2 writer writes schema version 3.
The official implementation may migrate schema 1 and 2 archives in place to schema 3 on open.
A .farchive written by a 2.x writer in schema 3 MUST remain readable by later 2.x releases.
If a database schema version is newer than the library supports, opening MUST fail clearly rather than risk silent corruption.
One Farchive instance is not thread-safe and must not be used concurrently across threads without external synchronization.
On POSIX platforms, file locking serializes writers across processes.
On platforms without fcntl, the archive falls back to no file lock. This is safe only when the caller ensures a single writer at a time.
Any future async API is an adapter over the same semantic model and on-disk format.
Farchive is not:
- a crawler
- a fetch scheduler
- a semantic diff engine
- a DVCS commit graph
- a generalized rsync transport
- a distributed sync engine
- a domain-specific archival standard
It stores what was observed, not all possible inferences about what was true between observations.
Farchive v2 is a local, content-addressed, history-preserving archive for opaque bytes observed at locators over time. It combines:
- exact raw-byte identity by SHA-256
- locator-scoped contiguous span history
- optional append-only event audit
- transparent inline compression
- storage-class dictionaries
- locator-local depth-1 deltas
- explicit maintenance chunking for large-blob dedup
while keeping the semantic core small, queryable, and stable.