How manifest.json uses hash pointers to reference document content stored in the chunk store — the indirection chain from metadata to text.
Xudanu separates metadata (the manifest) from content (the chunk store). The manifest is a single JSON file that acts as an index — it knows about every work, club, link, and edition, but it doesn't hold the actual text. Instead, it stores hash pointers that reference content blocks in the chunk store.
This is the same principle as a database index pointing to data pages, or a Git tree object pointing to blobs — content-addressed storage where the SHA-256 hash of the data IS its address.
Figure 1: The manifest stores metadata and hash pointers. The chunk store holds the actual document content, addressed by SHA-256 hash.
File: manifest.json
Contains: IDs, club definitions, link definitions, work metadata, admin state, counters, federation state, trails, blob metadata, historical authors.
Does NOT contain: The actual document text, span content, or provenance data.
Format: Pretty-printed JSON with SHA-256 checksum, versioned (format_version: 4), atomic writes with fsync.
Directory: chunks/
Contains: Content-addressed blobs — edition entries (text), provenance, edition roots, blob data.
Key: SHA-256 hash of serialized content (32 bytes).
Format: Tagged binary — 0x4A for JSON, 0x50 for postcard. Old format had no tag.
Because chunks are content-addressed, identical content across multiple works is automatically deduplicated. If two works contain the same paragraph, there's only one copy in the chunk store.
When the server renders a document, it follows a chain of pointers from the manifest down to the actual text characters. Each arrow is a hash lookup in the chunk store:
Figure 2: The indirection chain from manifest to text. Each blue box lives in the manifest; each green box is a chunk in the content-addressed store.
The manifest struct (src/persist/manifest.rs:156) contains these top-level fields, organized by the version that introduced them:
| Field | Type | Purpose |
|---|---|---|
format_version | u32 | Schema version (currently 4) |
sequence | u64 | Monotonic write counter |
checksum | String | SHA-256 of all fields (excluding self, timestamp, version) |
created_at | String | ISO timestamp |
server_version | String | Cargo version that wrote this manifest |
manifest_slot | char | 'a' or 'b' for dual-write fault tolerance |
| Field | Purpose |
|---|---|
grand_map_id_counter | Next ID to allocate |
session_counter | Session counter |
operation_counter | Global operation counter |
system_clubs | public/admin/access/empty club IDs |
works | Vec<WorkEntry> — each has a WorkChunkRef pointer |
clubs | Vec<ClubChunkRef> |
standalone_editions | Vec<StandaloneEditionChunkRef> |
admin | accepting_connections, grants |
key_history | Crypto key rotation history |
| Field | Purpose |
|---|---|
links | Vec<LinkEntry> — inline link definitions (origin, destination, types) |
links_hash | Option<[u8;32]> — backup chunk for links |
link_counter | Next link ID |
| Field | Purpose |
|---|---|
reconcile_store | Cross-server merge state |
reconcile_counter | Reconciliation counter |
federation | Optional FederationSnapshot |
| Field | Purpose |
|---|---|
content_address | Content hash → work index |
blob_metas / blob_metas_hash | Binary blob metadata (inline + backup chunk) |
historical_authors / historical_authors_hash | Author registry (inline + backup chunk) |
annotations_hash | Annotation data chunk |
fossil_snapshots_hash | Fossil snapshot chunk |
starred_works | HashMap<club, HashSet<work>> |
trails / trail_counter | Ordered reading paths through documents |
compound_editions | Vec<(BeId, CompoundEdition)> |
Each work in the manifest has a WorkEntry that holds metadata about the work and a pointer (WorkChunkRef) to its content tree:
pub struct WorkEntry {
be_id: BeId, // unique ID for this work
work_ref: WorkChunkRef, // → pointer into chunk store
is_source: bool, // imported from external source?
source_author_id: Option<BeId>, // original author
source_edition_info: Option<String>, // edition label
content_start_line: Option<u64>, // for partial imports
content_end_line: Option<u64>,
source_fingerprint: Option<Vec<u64>>, // recomputed on restore
}
The WorkChunkRef is the pointer. It lives only in the manifest — it's never serialized as a chunk itself. It tells the server where to find this work's content:
pub struct WorkChunkRef {
be_id: BeId, // same as WorkEntry.be_id
owner: Option<BeId>, // owning club
revision_count: u64, // how many editions exist
current_root: EditionChunkRef, // → LIVE edition (the one users see)
history: BTreeMap<u64, EditionChunkRef>, // → OLD editions (revision → ref)
read_club: Option<BeId>, // who can read
edit_club: Option<BeId>, // who can edit
sponsors: Vec<BeId>, // sponsoring clubs
endorsements: Vec<(u64, u64)>, // (club_id, token_id) pairs
}
The smallest pointer unit. Just a hash and a count:
pub struct EditionChunkRef {
root_hash: [u8; 32], // SHA-256 of the EditionRootChunk
entry_count: u32, // number of entries (for sanity check)
}
The root_hash is looked up in the chunk store to load the EditionRootChunk.
Three chunk types live inside the chunk store. They form a tree:
struct EditionRootChunk {
default: Option<RangeElement>,
domain_start: Option<i64>,
domain_infinite_above: bool,
entry_count: u32,
entry_chunk_hashes: Vec<[u8;32]>,
provenance_hash: Option<[u8;32]>,
}
The root of the edition's B-tree. Contains an array of up to 256 hashes pointing to EntryChunks.
struct EntryChunk {
entries: Vec<(i64, RangeElement)>,
provenances: Vec<Option<
ElementProvenance>>,
}
This is where the actual text lives. Each entry is (position, RangeElement). Up to 256 entries per chunk.
struct ProvenanceChunk {
spans: Vec<SpanProvenance>,
}
Tracks who wrote what — which club/author created each span of text. This is the attribution data that powers the "via work:XXXX" labels and endorsement chains. Optional: small documents may have no provenance chunk.
0x4A (‘J’) for JSON, 0x50 (‘P’) for Postcard (compact binary). Older chunks (pre-v0.7.6) have no tag — this is a breaking change.
Transclusion links are the most safety-critical data — losing them means losing the connections between documents. Xudanu stores links in three redundant layers:
Figure 3: Three-layer link persistence. Links survive crashes through WAL journaling, inline manifest storage, and content-addressed backup chunks.
The restore_from_data_dir() function follows this sequence:
Parse JSON, verify checksum matches content. If checksum fails, try backup manifests (manifest_v{N}.json) or dual-slot (manifest_a/b.json).
If format_version < 4, run migration chain (migrate_manifest_to_latest()). Old version is backed up as manifest.json.v{old}.bak.
Read wal.journal. Replay any entries with sequence number higher than the manifest's sequence. This catches operations that happened after the last manifest checkpoint.
For each WorkEntry in the manifest, follow work_ref.current_root.root_hash into the chunk store to load the EditionRootChunk. This rebuilds the live edition tree in memory.
Load manifest.links inline array into the link store. Cross-check against links_hash chunk if present.
Load trails, starred works, compound editions, blob metadata, historical authors, federation state.
my-data/ │ ├── manifest.json # The index file (metadata + pointers) ├── manifest_v{N}.json # Sequence-numbered backups (keep last 3) ├── manifest_a.json # Dual-slot A backup ├── manifest_b.json # Dual-slot B backup │ ├── wal.journal # Write-ahead log (append-only) │ ├── chunks/ # Content-addressed chunk store │ ├── a1/b2/a1b2c3d4e5f6... # EntryChunk (text content) │ ├── d4/e5/d4e5f6a7b8c9... # EditionRootChunk │ ├── g7/h8/g7h8i9j0k1l2... # ProvenanceChunk │ └── ... # (sharded by first 2 bytes of hash) │ ├── blobs/ # Binary blob data (images, files) │ └── {hash}.bin # Metadata in manifest.blob_metas │ ├── server.key # Ed25519 signing key (encrypted) │ ├── attribution.log # Tamper-evident provenance chain │ └── federation/ # Federation state (if enabled) └── peers.json
chunks/a1/b2/a1b2c3d4e5f6.... This avoids having 100,000+ files in a single directory.
The format_version field tracks schema changes. The migration chain lives in src/persist/migrations.rs:
pub const CURRENT_MANIFEST_VERSION: u32 = 4; // Version history: // v1 — baseline: works, clubs, editions, admin, key_history // v2 — added: links, links_hash, link_counter // v3 — added: federation, reconcile_store // v4 — added: content_address, blob_metas, historical_authors, // annotations, fossil_snapshots, social features (starred, trails) // v5+ — (future, not yet used)
Migrations always go forward. If format_version is higher than the binary supports, startup fails with a clear error telling you to upgrade.
Additive changes (new fields with #[serde(default)]) need zero migration code. Serde handles missing fields gracefully. The first checkpoint after upgrade writes the new fields.
v4_to_v5(raw_json) → raw_json. The old file is backed up as .v{old}.bak before migration.
Separate from the manifest version, the chunk store has its own format versioning. Since v0.7.6, every chunk is prefixed with a format tag byte:
pub const CHUNK_FORMAT_JSON: u8 = 0x4A; // 'J' = JSON pub const CHUNK_FORMAT_POSTCARD: u8 = 0x50; // 'P' = Postcard (binary)
The WAL journal also has a version header since v0.7.6:
pub const WAL_VERSION: u32 = 1; // WAL file format: // [4 bytes: WAL_VERSION = 1] [entries...] [entries...]
| Component | Versioned? | How |
|---|---|---|
| Manifest | Yes | format_version + migration chain |
| Metadata chunks | Yes | Tag byte 0x4A |
| Edition chunks | Yes | Tag byte 0x50 |
| WAL journal | Yes | WAL_VERSION header (u32) |
| Federation wire | Yes | Compatibility window + min_compat_version |
| Client wire | Yes | PROTOCOL_VERSION |
| Type | Contains | Points To | Source |
|---|---|---|---|
WorkChunkRef | current_root + history | EditionChunkRef(s) | manifest.rs |
EditionChunkRef | root_hash [u8;32] + entry_count | EditionRootChunk | edition_chunks.rs:14 |
EditionRootChunk | entry_chunk_hashes[] + provenance_hash | EntryChunk(s) + ProvenanceChunk | edition_chunks.rs:50 |
EntryChunk | entries: Vec<(i64, RangeElement)> | the actual text | edition_chunks.rs:36 |
ProvenanceChunk | spans: Vec<SpanProvenance> | attribution data | edition_chunks.rs:44 |
[u8; 32] hash fields | links_hash, blob_metas_hash, etc. | Backup/metadata chunks | manifest.rs |
| File | What It Implements |
|---|---|
src/persist/manifest.rs | Manifest struct, read/write, checksum, preflight, dual-slot |
src/persist/edition_chunks.rs | WorkChunkRef, EditionChunkRef, chunk serialization |
src/persist/chunk_store.rs | Content-addressed store, format tags |
src/persist/wal.rs | Write-ahead log |
src/persist/migrations.rs | Manifest migration chain |