Skip to main content

urna_runtime/
inspect.rs

1//! `MmapUrnaFile::inspect_json`: the `urna inspect --json` document. Kept out
2//! of `mmap_file.rs`, which opens and validates the file; this is pure
3//! presentation (re-parse the mmap, dump header + section table +
4//! manifest + hashes + simd backend), no search math.
5
6use urna_format::UrnaError;
7use urna_format::layout::SECTION_CHUNKS_CANONICAL;
8use urna_format::reader::UrnaView;
9use urna_format::sections::decode_chunks_canonical;
10
11use crate::error::RuntimeError;
12use crate::mmap_file::MmapUrnaFile;
13
14impl MmapUrnaFile {
15    /// Decode the stored canonical text for every chunk, in file order. this
16    /// is the TIER-1 citation text, the same bytes `urna cite` returns, NOT
17    /// the original source bytes. re-parses the mmap and decodes the
18    /// chunks_canonical (0x02) section (handles zstd / txt_streams / dict /
19    /// fsst transparently). used by the agent-native `retrieve()` to attach
20    /// cited text to each hit.
21    pub fn canonical_texts(&self) -> Result<Vec<String>, RuntimeError> {
22        let view = UrnaView::from_bytes(&self._mmap)?;
23        let n = view.header.n_chunks as usize;
24        decode_chunks_canonical(&view.decoded_section(SECTION_CHUNKS_CANONICAL)?, n)
25            .map_err(RuntimeError::Format)
26    }
27
28    /// The per-chunk ids in file order, parallel to `canonical_texts()`.
29    /// lets a caller build a chunk_id -> canonical-text map without re-reading
30    /// the file (the ids are already decoded and held at open time).
31    pub fn chunk_ids(&self) -> &[String] {
32        &self.chunk_ids
33    }
34
35    /// Re-parse the mmap and return a JSON document mirroring `urna
36    /// inspect`: header fields, section table entries, manifest, hashes,
37    /// and the runtime SIMD backend.
38    pub fn inspect_json(&self) -> Result<String, RuntimeError> {
39        let view = UrnaView::from_bytes(&self._mmap)?;
40        let magic = std::str::from_utf8(&view.header.magic)
41            .unwrap_or("")
42            .to_string();
43        let sections: Vec<serde_json::Value> = view
44            .section_table
45            .iter()
46            .map(|e| {
47                let name = urna_format::layout::section_name(e.section_id).unwrap_or("unknown");
48                serde_json::json!({
49                    "section_id": e.section_id,
50                    "name": name,
51                    "encoding": e.encoding,
52                    "offset": e.offset,
53                    "size": e.size,
54                    "checksum": hex::encode(e.checksum),
55                })
56            })
57            .collect();
58        let doc = serde_json::json!({
59            "magic": magic,
60            "version_major": view.header.version_major,
61            "version_minor": view.header.version_minor,
62            "format_version": view.manifest.format_version,
63            "schema_version": view.manifest.schema_version,
64            "embedding_dim": view.header.embedding_dim,
65            "n_chunks": view.header.n_chunks,
66            "n_embeddings": view.header.n_embeddings,
67            "file_size": view.header.file_size,
68            "manifest": view.manifest,
69            "sections": sections,
70            "blobs": self.blob_refs_json(),
71            "spaces": self.spaces_json(),
72            "file_hash": view.file_hash_hex(),
73            "content_hash": view.content_hash_hex()?,
74            "simd_backend": self.simd_backend().name(),
75        });
76        serde_json::to_string(&doc).map_err(|e| RuntimeError::Format(UrnaError::Json(e)))
77    }
78
79    /// The space_table (0x15) as a JSON array (`null` when the file has no
80    /// multimodal capability): one object per named space with its band
81    /// geometry, so `inspect --json` consumers (stats, the model bench)
82    /// see every queryable space without opening the bands.
83    fn spaces_json(&self) -> serde_json::Value {
84        match &self.spaces {
85            None => serde_json::Value::Null,
86            Some(spaces) => spaces
87                .iter()
88                .map(|s| {
89                    serde_json::json!({
90                        "name": s.entry.name,
91                        "space_index": s.entry.space_index,
92                        "dim": s.entry.dim,
93                        "dtype": s.entry.dtype_str(),
94                        "model_hash": s.entry.model_hash,
95                        "n_vectors": s.entry.n_vectors,
96                        "band_bytes": s.size,
97                    })
98                })
99                .collect(),
100        }
101    }
102
103    /// The blob_refs (0x14) table as a JSON array (`null` when the file
104    /// has no blob capability): one object per blob with its content hash
105    /// as `sha256:<hex>`, the uri hint, original byte length, and whether
106    /// the bytes are inlined in this .urna.
107    fn blob_refs_json(&self) -> serde_json::Value {
108        match self.blob_refs() {
109            None => serde_json::Value::Null,
110            Some(refs) => refs
111                .iter()
112                .map(|r| {
113                    serde_json::json!({
114                        "content_hash": format!("sha256:{}", hex::encode(r.content_hash)),
115                        "original_uri": r.original_uri,
116                        "byte_len": r.byte_len,
117                        "inlined": r.inlined,
118                    })
119                })
120                .collect(),
121        }
122    }
123}