Manifest & Chunk Architecture

How manifest.json uses hash pointers to reference document content stored in the chunk store — the indirection chain from metadata to text.

1. Architecture Overview 2. Two-Layer Storage Model 3. The Indirection Chain 4. Manifest Fields 5. Work → Edition Tree 6. Chunk Types 7. Three-Layer Link Persistence 8. What Happens on Restart 9. Data Directory Layout 10. Versioning & Migration

1. Architecture Overview

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.

DATA DIRECTORY manifest.json works[] clubs[] links[] compound_editions[] blob_metas[] admin, key_history, ... metadata + pointers no actual text stored here hash pointers chunks/ a1b2c3d4... EntryChunk d4e5f6a7... EditionRoot g7h8i9j0... Provenance k1l2m3n4... EntryChunk content-addressed by SHA-256 actual text lives here deduplicated & immutable

Figure 1: The manifest stores metadata and hash pointers. The chunk store holds the actual document content, addressed by SHA-256 hash.

2. Two-Layer Storage Model

Layer 1: Manifest

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.

Layer 2: Chunk Store

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.

Why this separation? The manifest is small (typically < 1MB even for thousands of documents). It loads entirely into memory at startup. The chunk store holds the bulk of the data — potentially gigabytes of text — and is accessed lazily. Only chunks needed for rendering, editing, or transclusion are loaded from disk.

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.

3. The Indirection Chain

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:

manifest.json works: Vec<WorkEntry> WorkEntry.work_ref: WorkChunkRef current_root: EditionChunkRef { root_hash: [u8;32] } hash lookup CHUNK STORE EditionRootChunk entry_chunk_hashes: Vec<[u8;32]> provenance_hash: Option<[u8;32]> EntryChunk entries: Vec<(i64, RangeElement)> = the actual text ProvenanceChunk spans: Vec<SpanProvenance> who wrote what Each box is a separate chunk. The root knows its children's hashes. This is a Merkle tree — tampering with content changes its hash, breaking the chain.

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.

Key insight: Each box in the chunk store is a separate chunk. The manifest only knows the hash of the root. From there, the root chunk knows the hashes of its children. This is a Merkle tree — any tampering with content changes its hash, breaking the chain.

4. Manifest Fields

The manifest struct (src/persist/manifest.rs:156) contains these top-level fields, organized by the version that introduced them:

Envelope Fields

FieldTypePurpose
format_versionu32Schema version (currently 4)
sequenceu64Monotonic write counter
checksumStringSHA-256 of all fields (excluding self, timestamp, version)
created_atStringISO timestamp
server_versionStringCargo version that wrote this manifest
manifest_slotchar'a' or 'b' for dual-write fault tolerance

v1 Core: Identity & Content

FieldPurpose
grand_map_id_counterNext ID to allocate
session_counterSession counter
operation_counterGlobal operation counter
system_clubspublic/admin/access/empty club IDs
worksVec<WorkEntry> — each has a WorkChunkRef pointer
clubsVec<ClubChunkRef>
standalone_editionsVec<StandaloneEditionChunkRef>
adminaccepting_connections, grants
key_historyCrypto key rotation history

v2: Transclusion Links

FieldPurpose
linksVec<LinkEntry> — inline link definitions (origin, destination, types)
links_hashOption<[u8;32]> — backup chunk for links
link_counterNext link ID

v3: Federation

FieldPurpose
reconcile_storeCross-server merge state
reconcile_counterReconciliation counter
federationOptional FederationSnapshot

v4: Metadata & Social

FieldPurpose
content_addressContent hash → work index
blob_metas / blob_metas_hashBinary blob metadata (inline + backup chunk)
historical_authors / historical_authors_hashAuthor registry (inline + backup chunk)
annotations_hashAnnotation data chunk
fossil_snapshots_hashFossil snapshot chunk
starred_worksHashMap<club, HashSet<work>>
trails / trail_counterOrdered reading paths through documents
compound_editionsVec<(BeId, CompoundEdition)>

5. Work → Edition Tree

WorkEntry

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
}

WorkChunkRef

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
}

EditionChunkRef

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.

6. Chunk Types

Three chunk types live inside the chunk store. They form a tree:

EditionRootChunk

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.

EntryChunk

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.

ProvenanceChunk

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.

Chunk format tags (v0.7.6+): Every chunk in the store is prefixed with a 1-byte format tag. 0x4A (‘J’) for JSON, 0x50 (‘P’) for Postcard (compact binary). Older chunks (pre-v0.7.6) have no tag — this is a breaking change.

7. Three-Layer Link Persistence

Transclusion links are the most safety-critical data — losing them means losing the connections between documents. Xudanu stores links in three redundant layers:

LAYER 1 — WAL (Write-Ahead Log) wal.journal Every link creation is appended BEFORE the operation is acknowledged. On crash, WAL replays entries with sequence > manifest.sequence. checkpoint (~30s) LAYER 2 — Manifest Inline manifest.json → links: Vec<LinkEntry> Links stored inline in the manifest JSON. Read directly on restore. No chunk store lookup needed. checkpoint (backup) LAYER 3 — Manifest Hash Pointer manifest.json → links_hash: [u8;32] → chunk store Links also serialized as a chunk. Provides checksummed redundancy. If inline array is corrupted, recover from chunk store copy. RESTORE PRIORITY 1. Manifest chunk (links_hash) — most durable, content-addressed 2. Manifest inline (links array) — convenient, always loaded    3. WAL replay — catches post-checkpoint writes

Figure 3: Three-layer link persistence. Links survive crashes through WAL journaling, inline manifest storage, and content-addressed backup chunks.

8. What Happens on Restart

The restore_from_data_dir() function follows this sequence:

Step 1: Read manifest.json

Parse JSON, verify checksum matches content. If checksum fails, try backup manifests (manifest_v{N}.json) or dual-slot (manifest_a/b.json).

Step 2: Check format_version & migrate if needed

If format_version < 4, run migration chain (migrate_manifest_to_latest()). Old version is backed up as manifest.json.v{old}.bak.

Step 3: Replay WAL

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.

Step 4: Rebuild in-memory state

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.

Step 5: Restore links

Load manifest.links inline array into the link store. Cross-check against links_hash chunk if present.

Step 6: Restore social data

Load trails, starred works, compound editions, blob metadata, historical authors, federation state.

9. Data Directory Layout

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
Chunk file naming: Chunks are stored in a 2-level sharded directory structure based on the first 4 hex characters of the hash: chunks/a1/b2/a1b2c3d4e5f6.... This avoids having 100,000+ files in a single directory.

10. Versioning & Migration

Manifest Version

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)

Forward Only

Migrations always go forward. If format_version is higher than the binary supports, startup fails with a clear error telling you to upgrade.

Additive = Zero Code

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.

Breaking changes (renamed/removed fields, type changes) require a migration function in the chain: v4_to_v5(raw_json) → raw_json. The old file is backed up as .v{old}.bak before migration.

Chunk Format Tags

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...]

Versioning Coverage Summary

ComponentVersioned?How
ManifestYesformat_version + migration chain
Metadata chunksYesTag byte 0x4A
Edition chunksYesTag byte 0x50
WAL journalYesWAL_VERSION header (u32)
Federation wireYesCompatibility window + min_compat_version
Client wireYesPROTOCOL_VERSION

Quick Reference: Pointer Types

TypeContainsPoints ToSource
WorkChunkRefcurrent_root + historyEditionChunkRef(s)manifest.rs
EditionChunkRefroot_hash [u8;32] + entry_countEditionRootChunkedition_chunks.rs:14
EditionRootChunkentry_chunk_hashes[] + provenance_hashEntryChunk(s) + ProvenanceChunkedition_chunks.rs:50
EntryChunkentries: Vec<(i64, RangeElement)>the actual textedition_chunks.rs:36
ProvenanceChunkspans: Vec<SpanProvenance>attribution dataedition_chunks.rs:44
[u8; 32] hash fieldslinks_hash, blob_metas_hash, etc.Backup/metadata chunksmanifest.rs

Source Files

FileWhat It Implements
src/persist/manifest.rsManifest struct, read/write, checksum, preflight, dual-slot
src/persist/edition_chunks.rsWorkChunkRef, EditionChunkRef, chunk serialization
src/persist/chunk_store.rsContent-addressed store, format tags
src/persist/wal.rsWrite-ahead log
src/persist/migrations.rsManifest migration chain