Specification for Lithoglyph’s append-only journal format.
Status: PROPOSED (resolves Q-JOURNAL-ENTRY-001)
The journal is an append-only log of all mutations. Every change to the database is journaled before being applied to blocks. The journal provides:
-
Durability: Write-ahead logging for crash recovery
-
Reversibility: Every entry includes its inverse operation
-
Provenance: Complete audit trail with actor and rationale
lith.journal
├── Journal Header (4096 bytes = 1 block)
├── Segment 0 (variable, block-aligned)
├── Segment 1 (variable, block-aligned)
├── ...
└── Active Segment (being written)The journal header occupies the first block:
| Offset | Size | Field |
|---|---|---|
0 |
4 |
Magic: 0x4644424A (legacy |
4 |
4 |
Format version (current: 1) |
8 |
16 |
Database UUID (must match database) |
24 |
8 |
Head sequence (last committed) |
32 |
8 |
Checkpoint sequence (last checkpoint) |
40 |
8 |
Entry count |
48 |
8 |
File size (bytes) |
56 |
4008 |
Reserved |
Each journal entry has a fixed header followed by variable-length payloads.
| Offset | Size | Field | Description |
|---|---|---|---|
0 |
8 |
|
Monotonic sequence number (uint64) |
8 |
8 |
|
Unix microseconds (uint64) |
16 |
2 |
|
Operation type (see Operation Types) |
18 |
2 |
|
Entry flags (see Entry Flags) |
20 |
4 |
|
Forward payload length |
24 |
4 |
|
Inverse payload length |
28 |
4 |
|
Provenance payload length |
32 |
8 |
|
Primary block affected |
40 |
4 |
|
CRC32C of entire entry |
44 |
4 |
|
Total entry length (header + payloads) |
Journal Entry:
┌─────────────────────────────────────────┐ 0
│ sequence [8] │
├─────────────────────────────────────────┤ 8
│ timestamp [8] │
├─────────────────────────────────────────┤ 16
│ op_type [2] │ flags [2] │
├─────────────────────────────────────────┤ 20
│ forward_len [4] │ inverse_len [4] │
├─────────────────────────────────────────┤ 28
│ provenance_len [4]│ affected_block [8] │
├─────────────────────────────────────────┤ 40
│ checksum [4] │ entry_len [4] │
├─────────────────────────────────────────┤ 48
│ Forward Payload (variable) │
├─────────────────────────────────────────┤
│ Inverse Payload (variable) │
├─────────────────────────────────────────┤
│ Provenance Payload (variable) │
└─────────────────────────────────────────┘| Value | Name | Description |
|---|---|---|
0x0001 |
|
Insert document |
0x0002 |
|
Update document fields |
0x0003 |
|
Delete document |
0x0004 |
|
Replace entire document |
0x0010 |
|
Insert edge |
0x0011 |
|
Delete edge |
0x0012 |
|
Update edge properties |
0x0020 |
|
Create collection |
0x0021 |
|
Drop collection |
0x0022 |
|
Rename collection |
0x0030 |
|
Create/define schema |
0x0031 |
|
Alter schema |
0x0032 |
|
Drop schema |
0x0040 |
|
Add constraint |
0x0041 |
|
Drop constraint |
0x0050 |
|
Create index |
0x0051 |
|
Drop index |
0x0060 |
|
Begin migration |
0x0061 |
|
Migration step |
0x0062 |
|
Complete migration |
0x0063 |
|
Rollback migration |
0x0070 |
|
Checkpoint marker |
0x0071 |
|
Snapshot marker |
0xFF00 |
|
Explicitly irreversible (with rationale) |
| Bit | Name | Description |
|---|---|---|
0 |
|
Entry has been applied to blocks |
1 |
|
Entry was rolled back |
2 |
|
Entry is a checkpoint |
3 |
|
Payloads are LZ4-compressed |
4 |
|
No inverse (see IRREVERSIBLE type) |
5-15 |
Reserved |
Must be 0 |
The forward payload contains CBOR-encoded data describing what was done:
{
"collection": "evidence",
"document_id": "doc_abc123",
"fields": {
"claim": "Example claim",
"source": "ONS",
"prompt_score": 85
}
}The inverse payload contains CBOR-encoded data for undoing the operation:
| Operation | Inverse Payload |
|---|---|
|
|
|
|
|
|
|
|
|
|
The provenance payload is CBOR-encoded and MUST include:
{
"actor": {
"id": "user_12345",
"type": "human",
"name": "Alice Smith"
},
"rationale": "Adding evidence from ONS inflation report",
"source": {
"type": "api",
"endpoint": "/api/v1/documents",
"request_id": "req_abc123"
},
"timestamp": "2026-01-11T12:00:00.000000Z",
"session_id": "sess_xyz789",
"context": {
"investigation": "UK Inflation 2023",
"tags": ["economic", "primary-source"]
}
}-
Read journal header, verify magic and UUID
-
Scan forward from checkpoint sequence
-
For each entry:
-
Verify checksum
-
If
COMMITTEDflag set: entry already applied -
If
COMMITTEDflag not set: apply forward payload to blocks
-
-
Update superblock head sequence
-
Write new checkpoint
Entries are considered incomplete if:
-
Checksum doesn’t match
-
Entry length exceeds remaining file size
-
Payloads are truncated
Incomplete entries are discarded during recovery.
Checkpoints are written periodically:
JOURNAL seq=1000 op=CHECKPOINT
provenance: {actor: "system", rationale: "Periodic checkpoint"}
forward: {blocks_verified: 500, journal_size: 10485760}
inverse: {} (empty - checkpoints are irreversible markers)After a checkpoint:
-
Journal can be truncated up to checkpoint
-
Blocks before checkpoint are guaranteed consistent
JOURNAL seq=1234 op=DOC_INSERT timestamp=2026-01-11T12:00:00.000000Z
affected_block=42 flags=[COMMITTED]
forward: {
collection: "evidence"
document_id: "doc_abc123"
fields: {claim: "Example claim", source: "ONS"}
}
inverse: {
delete: {collection: "evidence", document_id: "doc_abc123"}
}
provenance: {
actor: {id: "user_12345", type: "human", name: "Alice Smith"}
rationale: "Adding evidence from ONS inflation report"
source: {type: "api", endpoint: "/api/v1/documents"}
}Golden test vectors in test-vectors/journal/:
-
header.bin- Valid journal header -
doc_insert.bin- Document insert entry -
doc_insert.txt- Canonical rendering -
doc_update_with_inverse.bin- Update with full inverse -
checkpoint.bin- Checkpoint entry -
incomplete_entry.bin- Truncated entry for recovery testing