Manifest
Every field of the .urna v1 manifest, the Capabilities and CapabilitiesExt flags, validation rules, JSON serialization and the additivity rule for new fields.
The manifest is the JSON object stored between the section table and the first payload. It declares the model, the dimension, the dtype and the search contract of the file. This page lists every field, the rules the reader enforces, and how new fields are added without breaking old files.
A real manifest, from the quickstart corpus:
{
"format_version": 1,
"schema_version": 1,
"embedding_model": "minishlab/potion-base-8M/v1",
"embedding_dim": 256,
"n_chunks": 12,
"dtype": "float32",
"metric": "ip",
"score_type": "cosine",
"normalize": "l2",
"index_type": "hnsw",
"rerank_policy": "exact",
"model_hash": "sha256:8f2eb91a754b4da59cdd8223d0ba196185fed1bb6f092fd2be0ff02b893b1c98",
"chunker_version": "quickstart/1",
"capabilities": {
"supports_exact": true,
"supports_ann": true,
"supports_bm25": true,
"supports_citations": true,
"supports_reproducible_build": true
},
"title": "quickstart",
"version": "0.1.0",
"created": "1970-01-01T00:00:00Z",
"capabilities_ext": {
"graph_present": true
}
}On disk it is compact JSON with no whitespace; urna inspect prints it indented.
Manifest fields
Rust struct urna_format::Manifest. "Default" is the value of Manifest::default().
| Field | Type | Required | Default | Rule |
|---|---|---|---|---|
format_version | u32 | yes | 1 | 1 or lower, else UnsupportedFormatVersion |
schema_version | u32 | yes | 1 | 1 or lower, else UnsupportedSchemaVersion |
embedding_model | String | yes | "" | not empty |
embedding_dim | u32 | yes | 0 | greater than 0; equals the header |
n_chunks | u64 | yes | 0 | greater than 0; equals the header and the chunk count at build |
dtype | String | yes | "float32" | float32, float16, int8 or int4 |
metric | String | yes | "ip" | only ip |
score_type | String | yes | "cosine" | cosine or hybrid_rrf |
normalize | String | yes | "l2" | only l2 |
index_type | String | yes | "exact" | exact, hnsw or hybrid |
rerank_policy | String | yes | "none" | none or exact; hnsw and hybrid require exact |
model_hash | String | yes | "" | sha256: followed by 64 hex digits |
chunker_version | String | yes | "" | not empty |
capabilities | Capabilities | yes | see below | see below |
title, version, created, description, license | Option<String> | no | unset, omitted | none; created is free text |
authors | Option<Vec<String>> | no | unset, omitted | none |
mrl_dim, full_dim | Option<u32> | no | unset, omitted | see Matryoshka fields |
capabilities_ext | Option<CapabilitiesExt> | no | unset, omitted | none |
| any other key | extra map | no | empty | kept and written back unchanged |
"Required" means the key must be present for the JSON to parse. Unknown top-level keys do not fail: they land in the flattened extra map and round-trip.
index_type routes queries. The CLI verbs ask, retrieve and search-text, and Python UrnaFile.retrieve, pick the search path from the declared index_type, not from which sections are present. urna.build and the forge never write hybrid; only the Rust builder's hybrid() does. See Search paths and the exact rerank.
model_hash
model_hash fingerprints the embedding model. Queries must come from the same model, and the CLI checks this before searching: see The model gate.
Validation checks the format only. The all-zero value sha256: followed by 64 zeros passes.
The zero placeholder model_hash is accepted on write
Neither UrnaFileBuilder, urna.build nor the reader rejects the placeholder sha256:0000...0000: a file with it builds, validates and opens. The refusal happens only at query time. urna ask and urna retrieve refuse to search such a file, and urna search-text refuses unless you pass --skip-model-hash-check. In Python the check is opt-in: only retrieve(..., expected_model_hash=...) and search_space(..., expected_model_hash=...) compare hashes, and none of them treats the placeholder specially. Rust exposes the value as MmapUrnaFile::model_hash() and does no check of its own. See Known limits.
Matryoshka fields
mrl_dim and full_dim record a prefix truncation of the source vectors. When set:
- both are present, or neither;
mrl_dimis greater than 0 and at mostfull_dim;mrl_dimequalsembedding_dim: the runtime strides byembedding_dim, so the stored dimension is the prefix dimension;- with
dtype = "int4",mrl_dimis a multiple of 64.
Queries must be at the truncated dimension. The Rust builder has no truncation method: truncate and renormalize the vectors yourself and set the three fields. Python urna.build(mrl_dim=K) does it for you.
Capabilities
Five plain booleans, all required when parsing. Unknown keys inside capabilities are dropped.
| Flag | Default | Rule |
|---|---|---|
supports_exact | true | must be true |
supports_ann | false | must be true when index_type = "hnsw"; set by hnsw_index() |
supports_bm25 | false | must be true when index_type = "hybrid"; set by bm25_index() and hybrid() |
supports_citations | true | not checked |
supports_reproducible_build | true | must be true; not tied to the builder's reproducible flag |
The runtime does not read supports_ann or supports_bm25: it opens HNSW and BM25 when their sections are present.
CapabilitiesExt
Rust path urna_format::manifest::CapabilitiesExt (not re-exported at the crate root). Every flag is Option<bool> and is omitted when unset; the whole object is omitted when no flag is set.
| Flag | Set by | Read by |
|---|---|---|
supports_multimodal | space_table(), space_band() | the runtime, to open 0x15 and the bands |
graph_present | graph_adjacency() | the runtime, to open 0x0C |
blobs_present | blob_refs(), blob_data(), blob_span_overlay() | the runtime, to open 0x14, 0x16 and 0x17 |
graph_entities_present | nothing | nothing |
supports_catalog | nothing | nothing |
Without its flag, the runtime ignores the section: a 0x0C graph without graph_present is not loaded and search_graph falls back to exact search.
Validation
Manifest::validate() checks in this order and returns the first failure: format_version, schema_version, embedding_model not empty, embedding_dim greater than 0, n_chunks greater than 0, chunker_version not empty, model_hash format, dtype, metric, score_type, normalize, index_type, rerank_policy, hnsw or hybrid with rerank_policy = "exact", supports_exact, supports_reproducible_build, hnsw with supports_ann, hybrid with supports_bm25, then the Matryoshka rules.
After the manifest parses and validates, the reader also requires embedding_dim and n_chunks to equal the header, and the search_contract section to agree with metric, score_type, normalize, index_type and rerank_policy. The error variants are on Typed errors.
Serialization
Manifest::to_canonical_json() writes compact JSON: known fields in declaration order (the order of the table above), then the extra keys sorted, with no whitespace. Nested objects keep insertion order. The output is deterministic, but it is not RFC 8785 (JCS), which sorts every key; comments in the crate that say "JCS" mean this declaration-order form.
The manifest has no checksum of its own and is not part of content_hash. Editing a manifest field changes file_hash and leaves every citation valid. See Hashes and citations.
Additivity rule
Format v1 grows without breaking old files:
- A new manifest field is an
Optionskipped when unset, so a file that does not use it serializes to the same bytes as before. - A new capability flag goes in
CapabilitiesExtor in theextramap, never as a new required boolean inCapabilities. A new required boolean would make every old manifest fail to parse and would change everyfile_hash. - Keys a reader does not know survive in
extra.
The test crates/urna-format/tests/manifest_additivity.rs guards these rules. See Compatibility.
Provenance and search_contract
Two related values live in sections, not in the manifest:
provenance(0x05) is any JSON value. The Rust builder defaults it to{}; set it withwith_provenance(value). It is canonical, so its bytes, key order included, movecontent_hash.reproducible(true)does not rewrite it.search_contract(0x06) is{metric, score_type, normalize, index_type, rerank_policy}, copied from the manifest at build time and cross-checked on read.
Media and spaces are not manifest fields
The .urna manifest has no media or spaces key. Inside the file, named spaces are the 0x15 table plus their bands, and media is 0x14, 0x16 and 0x17 (see Sections). The spaces and media keys you may have seen belong to the forge's sidecar <name>.manifest.json, a separate JSON file written next to the .urna by urna build; see Build artifacts. Keep "the file's manifest" and "the forge build manifest" apart.
Hashes and citations
Exact preimages of every integrity value in a .urna file: header and section checksums, the footer hash, file_hash, content_hash, chunk_id and the citation id.
Compatibility
Which .urna files a 0.5.1 reader accepts, how format v1 grows without breaking old files, legacy NEST files and what makes two builds byte-identical.