urna_format/layout/mod.rs
1//! .urna binary file layout (v1)
2//!
3//! File structure:
4//! ```text
5//! [0 .. 128) UrnaHeader (128 bytes)
6//! [128 .. 128+count*32) SectionTable (32 bytes per entry)
7//! [manifest_offset .. ...) Manifest JSON (JCS canonical)
8//! [sections ...] Required sections, each starting at a
9//! 64-byte aligned offset; padding before
10//! each section is zero and is NOT part of
11//! the section's checksum
12//! [file_size-40 .. file_size) Footer (40 bytes)
13//! ```
14//!
15//! All multi-byte integers are little-endian, unsigned unless noted.
16
17mod footer;
18mod header;
19mod section_entry;
20#[cfg(test)]
21mod tests;
22
23pub use footer::UrnaFooter;
24pub use header::UrnaHeader;
25pub use section_entry::SectionEntry;
26
27pub const URNA_MAGIC: &[u8; 4] = b"URNA";
28/// magic written by releases up to 0.4.0, before the project was renamed.
29/// the reader accepts it (same layout, same hashes); the writer never emits it.
30pub const LEGACY_MAGIC: &[u8; 4] = b"NEST";
31pub const URNA_VERSION_MAJOR: u16 = 1;
32pub const URNA_VERSION_MINOR: u16 = 0;
33pub const URNA_HEADER_SIZE: usize = 128;
34pub const URNA_SECTION_ENTRY_SIZE: usize = 32;
35pub const URNA_FOOTER_SIZE: usize = 40;
36
37/// Every section's `offset` is aligned to this many bytes. Padding
38/// before each section is zero and is NOT covered by the section's
39/// checksum. Chosen to match common SIMD widths so embeddings can be
40/// loaded directly from mmap.
41pub const SECTION_ALIGNMENT: u64 = 64;
42
43/// Round `n` up to the next multiple of `a`. `a` must be a power of two.
44#[inline]
45pub fn align_up(n: u64, a: u64) -> u64 {
46 debug_assert!(a.is_power_of_two(), "alignment must be a power of two");
47 (n + a - 1) & !(a - 1)
48}
49
50/// Section payload encoding.
51///
52/// - `0 = raw`: payload is the canonical bytes as the reader consumes them.
53/// Used for embeddings (float32) and any non-compressed metadata section.
54/// - `1 = zstd`: payload is zstd-compressed canonical bytes. Only valid for
55/// non-embedding sections; the reader transparently decompresses.
56/// - `2 = float16`: payload is `n * dim * 2` bytes of f16 LE; requires the
57/// manifest to declare `dtype = "float16"`. Only valid for the embeddings
58/// section.
59/// - `3 = int8`: payload is the int8 quantized embeddings section (per-vector
60/// f32 scales followed by i8 vectors); requires `dtype = "int8"`. Only
61/// valid for the embeddings section.
62/// - `7 = int4`: payload is the int4 block-64 quantized embeddings section
63/// (per-64-dim-group f16 absmax scales followed by packed 4-bit signed
64/// codes, two nibbles per byte, codes in `[-7, 7]`); requires `dtype =
65/// "int4"` and `embedding_dim` divisible by 64. Only valid for the
66/// embeddings section. STORED-PRECISION cosine, scored straight off mmap
67/// by the fused dequant+dot kernel (never zstd/shuffle), the first real
68/// sub-int8 size lever (~2x over int8).
69///
70/// A reader rejects unknown encodings with `UnsupportedSectionEncoding`.
71pub const SECTION_ENCODING_RAW: u32 = 0;
72pub const SECTION_ENCODING_ZSTD: u32 = 1;
73pub const SECTION_ENCODING_FLOAT16: u32 = 2;
74pub const SECTION_ENCODING_INT8: u32 = 3;
75
76// reserved additive wire encodings. ids 4-255 are reserved within frozen
77// format v1 (see docs/arc/arc.toml). claimed here as named
78// constants so each future codec ships as a small additive diff. NOT yet
79// implemented: decode_payload rejects them with UnsupportedSectionEncoding
80// until their codec module lands, so old and new readers agree.
81pub const SECTION_ENCODING_INTPACK: u32 = 4;
82pub const SECTION_ENCODING_ZSTD_DICT: u32 = 5;
83pub const SECTION_ENCODING_FRONTCODE: u32 = 6;
84pub const SECTION_ENCODING_INT4: u32 = 7;
85pub const SECTION_ENCODING_RABITQ: u32 = 8;
86pub const SECTION_ENCODING_FSST: u32 = 9;
87/// `10 = txt_streams`: the chunks_canonical (0x02) section's COMPRESSED
88/// form re-laid-out from one concatenated zstd-19 blob into N independently
89/// zstd-encoded streams (one per canonical string) behind an intpack offset
90/// table (O(1) single-chunk seek/reopen). decodes BYTE-IDENTICALLY to the
91/// raw chunks_canonical payload, so content_hash and urna:// citations are
92/// unchanged. only valid for non-embedding sections. this is the named
93/// prerequisite layout for the dict(5)/fsst(9) text levers; a per-chunk
94/// frame loses cross-chunk LZ context (small ratio cost today) but is where
95/// a trained dict/fsst can beat one big zstd-19 blob.
96pub const SECTION_ENCODING_TXT_STREAMS: u32 = 10;
97
98/// Format version of the binary layout. Bumped when the on-disk
99/// container changes (header/footer/section table layout).
100pub const URNA_FORMAT_VERSION: u32 = 1;
101
102/// Schema version of the manifest/contract. Bumped when manifest
103/// fields or required section semantics change.
104pub const URNA_SCHEMA_VERSION: u32 = 1;
105
106// Section IDs. The first six are required (v1 contract); the rest are
107// optional and only present when the manifest's `capabilities` declare
108// them.
109pub const SECTION_CHUNK_IDS: u32 = 0x01;
110pub const SECTION_CHUNKS_CANONICAL: u32 = 0x02;
111pub const SECTION_CHUNKS_ORIGINAL_SPANS: u32 = 0x03;
112pub const SECTION_EMBEDDINGS: u32 = 0x04;
113pub const SECTION_PROVENANCE: u32 = 0x05;
114pub const SECTION_SEARCH_CONTRACT: u32 = 0x06;
115pub const SECTION_HNSW_INDEX: u32 = 0x07;
116pub const SECTION_BM25_INDEX: u32 = 0x08;
117
118// additive optional sections 0x09-0x10, all EXCLUDED from content_hash
119// (which covers the canonical six only), so adding any of them never
120// invalidates a urna:// citation. status per id:
121// 0x09 embeddings_fp: READ by the runtime rerank (`rerank::FpSlab`) as the
122// full-precision source for a sub-int8 candidate slab; the writer
123// does not emit it yet (int4 today reranks at stored precision).
124// 0x0A dictionary: WRITTEN + READ, the trained zstd dictionary the
125// `zstd_dict` text codec decodes chunks_canonical against.
126// 0x0B dedup_map: WRITTEN + READ, the ordinal -> unique-string map the
127// `dedup` text codec expands to the byte-identical canonical payload.
128// 0x0C graph_adjacency: WRITTEN + READ, in OPTIONAL_SECTIONS below.
129// 0x0D-0x10: claimed names only, no codec and no manifest
130// capability yet; a reader rejects them via validate_encoding.
131// 0x0A / 0x0B resolve through the text codec (never by section_name) on
132// purpose: they are private companions of chunks_canonical, not user-facing
133// sections.
134pub const SECTION_EMBEDDINGS_FP: u32 = 0x09;
135pub const SECTION_DICTIONARY: u32 = 0x0A;
136pub const SECTION_DEDUP_MAP: u32 = 0x0B;
137pub const SECTION_GRAPH_ADJACENCY: u32 = 0x0C;
138pub const SECTION_CHUNK_SCALARS: u32 = 0x0D;
139pub const SECTION_TOKENIZER_MODEL: u32 = 0x0E;
140pub const SECTION_EDIT_JOURNAL: u32 = 0x0F;
141pub const SECTION_REPRO_MANIFEST: u32 = 0x10;
142
143// additive optional sections past 0x10 (see docs/arc/arc.toml), one disjoint
144// map so no two features claim the same id. ALL are EXCLUDED from
145// content_hash, so adding any of them never invalidates a urna:// citation.
146// 0x11-0x13 graph nodes / edge props / entity map: claimed names only.
147// 0x14 blob_refs, 0x15 space_table, 0x16 blob_span_overlay, 0x17
148// blob_data: WRITTEN + READ, in OPTIONAL_SECTIONS below.
149pub const SECTION_GRAPH_NODES: u32 = 0x11;
150pub const SECTION_GRAPH_EDGE_PROPS: u32 = 0x12;
151pub const SECTION_GRAPH_ENTITY_MAP: u32 = 0x13;
152pub const SECTION_BLOB_REFS: u32 = 0x14;
153pub const SECTION_SPACE_TABLE: u32 = 0x15;
154// blob-relative span overlay: REPLACES the illegal chunks_original_spans
155// v1->2 bump (spans is canonical + content-hashed + required). an excluded
156// optional section keyed by chunk ordinal, so self_contained and catalog
157// twins keep the SAME content_hash and old readers still open the file.
158pub const SECTION_BLOB_SPAN_OVERLAY: u32 = 0x16;
159// inlined blob bytes: the self-contained twin of the 0x14 catalog. a small
160// offset table parallel to blob_refs order, then the raw media bytes. RAW
161// (media is already codec-compressed), EXCLUDED from content_hash so the
162// embedded and sidecar twins keep the same citations.
163pub const SECTION_BLOB_DATA: u32 = 0x17;
164
165// per-space vector bands. each non-text embedding space gets one fixed-
166// stride 64-byte-aligned slab in 0x20-0x2F (NEVER zstd, scored by the
167// existing simd kernels) and an optional matching fp rerank source in
168// 0x30-0x3F. base + SPACE_BAND_LEN define each band; both excluded from
169// content_hash. space[0]=text stays the canonical embeddings(0x04).
170pub const SECTION_SPACE_EMBEDDINGS_BASE: u32 = 0x20;
171pub const SECTION_SPACE_EMBEDDINGS_FP_BASE: u32 = 0x30;
172pub const SPACE_BAND_LEN: u32 = 0x10;
173
174/// Canonical order for content_hash. Sorted alphabetically by name; this
175/// order is fixed by spec so adding new section IDs cannot reshuffle the
176/// hash. Keep this list and section IDs in sync.
177pub const CANONICAL_SECTIONS: &[(u32, &str)] = &[
178 (SECTION_CHUNK_IDS, "chunk_ids"),
179 (SECTION_CHUNKS_CANONICAL, "chunks_canonical"),
180 (SECTION_CHUNKS_ORIGINAL_SPANS, "chunks_original_spans"),
181 (SECTION_EMBEDDINGS, "embeddings"),
182 (SECTION_PROVENANCE, "provenance"),
183 (SECTION_SEARCH_CONTRACT, "search_contract"),
184];
185
186/// Required sections for a v1 .urna file. A reader rejects any file
187/// missing one of these with `MissingRequiredSection`.
188pub const REQUIRED_SECTIONS: &[(u32, &str)] = CANONICAL_SECTIONS;
189
190/// Optional sections — present when their corresponding capability is
191/// advertised in the manifest. They do NOT participate in content_hash
192/// (which is over the canonical six only) so adding an optional section
193/// to a corpus does not invalidate citations.
194pub const OPTIONAL_SECTIONS: &[(u32, &str)] = &[
195 (SECTION_HNSW_INDEX, "hnsw_index"),
196 (SECTION_BM25_INDEX, "bm25_index"),
197 // graph_adjacency (0x0C, G1): additive chunk-to-chunk csr. resolves via
198 // section_name but stays OUT of CANONICAL_SECTIONS (content_hash-excluded).
199 (SECTION_GRAPH_ADJACENCY, "graph_adjacency"),
200 // blob_refs (0x14) + blob_span_overlay (0x16): content-hash references to
201 // source media blobs and the per-chunk blob-relative span overlay. both
202 // resolve via section_name and stay OUT of CANONICAL_SECTIONS, so a
203 // self-contained media corpus keeps the content_hash of its text twin.
204 (SECTION_BLOB_REFS, "blob_refs"),
205 (SECTION_BLOB_SPAN_OVERLAY, "blob_span_overlay"),
206 // blob_data (0x17): inlined media bytes for the self-contained twin.
207 (SECTION_BLOB_DATA, "blob_data"),
208 // space_table (0x15): the multimodal per-space directory. resolves via
209 // section_name and stays OUT of CANONICAL_SECTIONS; the vector bands it
210 // describes (0x20-0x2F / 0x30-0x3F) resolve through the range check in
211 // section_name below and are likewise content_hash-excluded.
212 (SECTION_SPACE_TABLE, "space_table"),
213];
214
215pub fn section_name(id: u32) -> Option<&'static str> {
216 if (SECTION_SPACE_EMBEDDINGS_BASE..SECTION_SPACE_EMBEDDINGS_BASE + SPACE_BAND_LEN).contains(&id)
217 {
218 return Some("space_embeddings");
219 }
220 if (SECTION_SPACE_EMBEDDINGS_FP_BASE..SECTION_SPACE_EMBEDDINGS_FP_BASE + SPACE_BAND_LEN)
221 .contains(&id)
222 {
223 return Some("space_embeddings_fp");
224 }
225 CANONICAL_SECTIONS
226 .iter()
227 .chain(OPTIONAL_SECTIONS.iter())
228 .find(|(sid, _)| *sid == id)
229 .map(|(_, name)| *name)
230}
231
232/// Common prefix for all internal section payloads (12 bytes):
233/// u32 version (LE)
234/// u64 entry_count (LE)
235pub const SECTION_PAYLOAD_PREFIX_SIZE: usize = 12;
236pub const SECTION_PAYLOAD_VERSION: u32 = 1;