epiphany/crates/epiphany-bundle/README.md

102 lines
5.9 KiB
Markdown

# epiphany-bundle
The Epiphany `.musc` file format, implementing the normative requirements of
**Chapter 8 (File Format)** of the core specification (`spec/core_spec.pdf`).
This is Agent D's crate per `spec/QUICKSTART.md`. It depends on
`epiphany-determinism` (Agent A) and on **nothing else** — not on `epiphany-core`
(Agent B) or `epiphany-ops` (Agent C):
> bundles handle bytes, ops handles semantics. A canonical-base snapshot from the
> bundle's perspective is opaque bytes plus a frontier DVV; only `epiphany-ops`
> interprets it. — QUICKSTART
A bundle is a single file: a fixed 64-byte header at offset 0, two 256-byte
superblock slots, then a body of immutable, content-addressed chunks. The
superblocks are the only mutable on-disk objects. A commit appends new chunks,
writes a new manifest chunk, then flips the active superblock by writing the
*inactive* slot and durably flushing it — that flush is the commit point. Because
commits only ever **append** and touch the **inactive** slot, a crash can never
corrupt the active state.
## What's here
| Area | Items | Spec |
|------|-------|------|
| Prelude | `FixedHeader` (64 B, CRC-32C), `Superblock`/`CommitState` (256 B, CRC-32C), `select_active` | Ch. 8 §"The Bundle Layout", §"Superblock Selection" |
| Atomic commit | `Bundle::create`/`open`/`commit`, the 7-step protocol, cold-open path | Ch. 8 §"The Atomic Write Protocol", §"Streaming Reads" |
| Content addressing | `chunk_content_hash`/`chunk_id`, `ChunkRef`, `ChunkKind`, `CompressionAlgorithm`, domain separation | Ch. 8 §"Content Hashing", §"Chunks" |
| Manifest | `Manifest` (canonical_base ≠ acceleration_snapshots), `SnapshotRef`, `BlobRef`, `ProfileDeclaration`, `ExtensionDeclaration` | Ch. 8 §"The Manifest" |
| Retention | `RetentionPolicy` (first-class), `ProfileConstraints` | Ch. 8 §"Garbage Collection and Retention" |
| Op blocks | `pack_operation_blocks` (1 MiB soft target), `encode_block`/`decode_block` | Ch. 8 §"Operation Envelope Blocks" |
| Storage | `BlockStore`, `MemStore`, `FileStore` (real `fsync`), `FaultStore` (crash sim) | Ch. 8 §"Durable Writes" |
| Gates | `fuzz::run_crash_recovery_fuzz`, `fuzz::exhaustive_crash_check`, `fuzz::run_manifest_selection_harness` | QUICKSTART acceptance |
## The crash-recovery contract (the acceptance gate)
> Kill the process between any two syscalls in the commit protocol; reopen; the
> bundle must be valid in 100% of runs, and must recover to the previous
> generation when the crash precedes the durable flush. This is the most
> important single test in the entire prototype. — QUICKSTART, Agent D
Killing a real process between syscalls cannot be made deterministic, so the
fuzzer drives the commit against a `FaultStore` that distinguishes **live**
(page-cache) bytes from **durable** (survives-a-crash) bytes and can crash after
any chosen syscall — optionally *tearing* the in-flight superblock write, the
case the slot CRC must catch. After every simulated crash the bundle is reopened
from the durable image and must:
1. open successfully (never corrupt);
2. be at the previous generation **or** the new one, never anything else;
3. if the commit returned `Ok`, be at the new generation; and if the crash was
*clean* (the in-flight flush persisted nothing) and the commit did not
complete, be at the previous generation — the exact "recover to the previous
generation when the crash precedes the durable flush" property. (A torn final
flush may at a full prefix legitimately persist the whole superblock — the
genuine post-commit case — so the torn branch admits either generation.)
4. report no integrity anomaly;
5. have every canonical chunk present and hash-intact.
Two drivers exercise this: a randomized 10,000-iteration sweep, and an
*exhaustive* per-commit sweep that tests **every** syscall boundary crossed with
**every** tear point (clean, and torn at prefixes around the 252-byte CRC offset
and the 256-byte slot size). The second leaves no step of the protocol untested.
The companion `manifest_selection` gate asserts the Chapter 8 superblock-
selection rule across every corruption scenario the QUICKSTART enumerates: slot A
corrupt + B valid (and vice versa), both valid at generation+1, both valid at the
same generation (equivalent, and divergent), a generation gap > 1, a
non-committed slot, a manifest-hash mismatch, and neither valid.
## Building and testing
```sh
cargo test -p epiphany-bundle # unit + the two gates
cargo clippy -p epiphany-bundle --all-targets -- -D warnings
cargo run --release --example fuzz_crash -- 1000000 # extended crash soak
```
## Hand-off criteria (QUICKSTART, Agent D)
- [x] `cargo test` clean.
- [x] Crash-recovery fuzzer passes 10,000 iterations
(`crash_recovery_fuzz_ten_thousand_iterations`, two seeds; extended soak
via the example binary; exhaustive per-syscall sweep in
`exhaustive_sweep_across_base_states_and_commit_shapes`).
- [x] Manifest-selection harness handles every corruption scenario
(`every_selection_scenario_holds`).
- [x] Real-filesystem `fsync` round-trip (`file_store_real_fsync_round_trip`).
## Scope boundaries (per QUICKSTART "Don't do these")
v0 writes only uncompressed chunks (compression on the *write* path is deferred),
but *reading* zstd-compressed chunks and blobs is supported, per the spec's
§Compression MUST (the manifest is mandatory-uncompressed regardless, and a
compressed manifest is rejected). It carries the text-projection *root* but does
not implement the s-expression projection content, and it *preserves* extension
declarations and chunks but does not *evaluate* edit barriers — barrier operands
(`OperationKindTag`, `ObjectKind`, `EditBarrier`) are owned by Agents C and E.
Operation envelopes, snapshots, and causal frontiers are opaque bytes here.
See `DECISIONS.md` for the prototype byte-layout choices that anticipate the
deferred Binary Format companion, and the batched Pass 11 candidates.