Skip to main content

vole_document/field/
build.rs

1//! Direct source → field ingestion (Phase 17).
2//!
3//! The runtime path deliberately **skips the compression candidate search**. The
4//! field is not a compression product, and — this is the load-bearing fact — its
5//! observations are *not* derived from the winning reconstruction program: both
6//! Stage B ([`crate::field::ingest`]) and the package adapter
7//! ([`crate::field::ingest_package`]) recover exact spans, object/stream/page
8//! nodes, package members, resource blobs, and the hierarchical index by scanning
9//! the source bytes. Here the source bytes are the **original `field-build`
10//! input**, handed straight to the native scanners (`ingest_pdf_direct` /
11//! `ingest_package_direct`), not a copy materialized back out of the authority
12//! (Phase 18.2). The descriptor's program is consulted only for the advisory
13//! `OBSERVATION_INDEX` op table (a seek optimization; it falls back to
14//! the full descriptor path when it cannot apply) and for the exactness check
15//! itself (`FieldStore::ingest_verified` materializes the stored authority once
16//! and byte-compares it to the original source).
17//!
18//! Therefore a single, deterministically chosen, exact program serves the same
19//! observation surface as the searched winner. [`BuildProfile::Runtime`] fixes
20//! that program to the literal [`CandidateKind::Raw`] floor: one `EMIT_OBJECT`
21//! over one literal object. It costs the authority encoding, not a search, and it
22//! is proved exact by the same court ([`crate::encode::encode_with`]) the searched
23//! path uses — the court is still run, over exactly one candidate.
24//!
25//! This module produces the exact authority (a valid `.voldoc` descriptor blob,
26//! stored in the field's descriptor namespace and optionally written to disk) and
27//! the field from one source buffer, in one process.
28
29use crate::encode::candidates::CandidateKind;
30use crate::encode::encode_with_observation_index;
31use crate::error::{Error, Result};
32use crate::field::FieldId;
33use crate::field::FieldStore;
34use crate::field::ingest::IngestReport;
35use crate::field::ingest::ingest_pdf_direct;
36#[cfg(feature = "package")]
37use crate::field::ingest_package::{PackageIngestReport, ingest_package_direct};
38use crate::integrity::{sha256, to_hex};
39use crate::limits::Limits;
40use crate::parallel::WorkerPool;
41
42/// Fixed, non-searched reconstruction profiles for the direct ingest path.
43///
44/// A profile names a *deterministic function source → exact descriptor*, never a
45/// search. It exists so the runtime CLI can state which fixed program it chose;
46/// adding a profile is a deliberate act, and every profile must remain exact
47/// (proved per build by the complete-cost court) and must not reduce the
48/// observation surface the field exposes.
49#[derive(Debug, Clone, Copy, PartialEq, Eq)]
50pub enum BuildProfile {
51    /// The literal `RAW` floor: one literal object, one `EMIT_OBJECT`. No
52    /// candidate is searched; the court prices exactly this one program.
53    Runtime,
54}
55
56impl BuildProfile {
57    /// Stable lower-case name for reports and receipts.
58    pub const fn name(self) -> &'static str {
59        match self {
60            BuildProfile::Runtime => "runtime",
61        }
62    }
63
64    /// The single candidate family this profile fixes, with no search.
65    pub const fn candidate(self) -> CandidateKind {
66        match self {
67            BuildProfile::Runtime => CandidateKind::Raw,
68        }
69    }
70
71    /// Parse a `--profile` spelling.
72    pub fn parse(s: &str) -> Result<Self> {
73        match s {
74            "runtime" => Ok(BuildProfile::Runtime),
75            other => Err(Error::usage(format!(
76                "unknown --profile {other:?}; expected one of: runtime"
77            ))),
78        }
79    }
80}
81
82/// Which native inverse compiler ran for a direct build.
83#[derive(Debug, Clone, PartialEq, Eq)]
84pub enum DirectIngest {
85    /// A PDF (or opaque non-ZIP) field.
86    Pdf(IngestReport),
87    /// A ZIP-based package (DOCX/EPUB/ODT/generic ZIP) field.
88    #[cfg(feature = "package")]
89    Package(PackageIngestReport),
90}
91
92impl DirectIngest {
93    /// The id of the field the ingest produced.
94    pub fn field_id(&self) -> FieldId {
95        match self {
96            DirectIngest::Pdf(r) => r.field,
97            #[cfg(feature = "package")]
98            DirectIngest::Package(r) => r.field,
99        }
100    }
101}
102
103/// What one direct (non-searched) build produced.
104#[derive(Debug, Clone, PartialEq, Eq)]
105pub struct DirectBuildReport {
106    /// The fixed profile used (`runtime`).
107    pub profile: &'static str,
108    /// The fixed candidate family actually serialized (`RAW`).
109    pub candidate: &'static str,
110    /// Candidates priced by the court. Always `1` for a fixed profile; the field
111    /// is present so a receipt can witness that no search happened.
112    pub candidates_evaluated: u32,
113    /// Lower-case hex SHA-256 of the source (from the court's own check).
114    pub source_sha256: String,
115    /// Exact source length.
116    pub source_len: u64,
117    /// Length of the exact authority actually stored in the field (the serialized
118    /// descriptor, including any advisory observation-index record the ingest
119    /// added). This is the blob `--voldoc` writes and a later `field-ingest`
120    /// would consume.
121    pub encoded_len: u64,
122    /// Lower-case hex SHA-256 of the stored exact authority.
123    pub descriptor_sha256: String,
124    /// The field ingest outcome (format, field id, recovered structure counters).
125    pub ingest: DirectIngest,
126}
127
128/// Build a field directly from source bytes with a fixed, non-searched program.
129///
130/// Serial `None`-pool form; see [`build_field_with`].
131pub fn build_field(
132    source: &[u8],
133    store: &mut FieldStore,
134    limits: Limits,
135    profile: BuildProfile,
136) -> Result<DirectBuildReport> {
137    build_field_with(source, store, limits, profile, None)
138}
139
140/// Build a field directly from source bytes with a fixed, non-searched program.
141///
142/// The exact authority is the descriptor returned by the court for
143/// `profile.candidate()` — one candidate, serialized, decoded, and byte-compared
144/// against `source`, so `materialize(descriptor) == source` holds by the same
145/// proof the searched path uses. The descriptor is then inverted into the field
146/// exactly as `field-ingest` would invert it.
147pub fn build_field_with(
148    source: &[u8],
149    store: &mut FieldStore,
150    limits: Limits,
151    profile: BuildProfile,
152    pool: Option<&WorkerPool>,
153) -> Result<DirectBuildReport> {
154    // The durable authority is the descriptor *with* the advisory
155    // observation-index record. The index is attached to the descriptor before
156    // the court serializes it (Phase 18.3), so the source-sized payload is
157    // serialized once rather than twice: the bytes are identical to enriching the
158    // plain `encode` output after the fact, but the extra parse + reserialize of
159    // the whole descriptor is removed from the direct-build wall.
160    let (authority, encode_report) =
161        encode_with_observation_index(source, limits, profile.candidate())?;
162    let encoded_len = authority.len() as u64;
163    let descriptor_sha256 = to_hex(&sha256(&authority));
164    // The direct path already holds the exact source, so hand it to the native
165    // scanner instead of reconstructing it from the authority. The authority is
166    // still stored and still verified to materialize to `source` *inside* the
167    // ingest (`FieldStore::ingest_verified`), so the Phase-18.2 one-pass shape is
168    // exact by construction: source → (authority stored + source scanned), with a
169    // single verification round trip instead of the previous two-to-three.
170    let ingest = direct_ingest(store, &authority, source, limits, pool)?;
171    Ok(DirectBuildReport {
172        profile: profile.name(),
173        candidate: encode_report.kind.name(),
174        candidates_evaluated: encode_report.candidates_evaluated,
175        source_sha256: encode_report.sha256_hex,
176        source_len: encode_report.source_len,
177        encoded_len,
178        descriptor_sha256,
179        ingest,
180    })
181}
182
183#[cfg(feature = "package")]
184fn direct_ingest(
185    store: &mut FieldStore,
186    authority: &[u8],
187    source: &[u8],
188    limits: Limits,
189    pool: Option<&WorkerPool>,
190) -> Result<DirectIngest> {
191    // Detect the family from the *original bytes* (never a file name) and invert
192    // with the matching adapter; both scan `source` directly rather than a
193    // materialized copy.
194    if crate::field::document_format::is_zip(source, limits) {
195        Ok(DirectIngest::Package(ingest_package_direct(
196            store, authority, source, limits, pool,
197        )?))
198    } else {
199        Ok(DirectIngest::Pdf(ingest_pdf_direct(
200            store, authority, source, limits, pool,
201        )?))
202    }
203}
204
205#[cfg(not(feature = "package"))]
206fn direct_ingest(
207    store: &mut FieldStore,
208    authority: &[u8],
209    source: &[u8],
210    limits: Limits,
211    pool: Option<&WorkerPool>,
212) -> Result<DirectIngest> {
213    Ok(DirectIngest::Pdf(ingest_pdf_direct(
214        store, authority, source, limits, pool,
215    )?))
216}
217
218#[cfg(test)]
219mod tests {
220    use super::*;
221
222    fn temp_root(label: &str) -> std::path::PathBuf {
223        let mut p = std::env::temp_dir();
224        p.push(format!(
225            "vole-build-{label}-{}-{}",
226            std::process::id(),
227            std::time::SystemTime::now()
228                .duration_since(std::time::UNIX_EPOCH)
229                .unwrap()
230                .as_nanos()
231        ));
232        std::fs::create_dir_all(&p).unwrap();
233        p
234    }
235
236    #[test]
237    fn runtime_profile_prices_one_candidate_and_is_exact() {
238        let source: Vec<u8> = b"the runtime path must not search the portfolio\n".repeat(64);
239        let root = temp_root("exact");
240        let mut store = FieldStore::open(&root).unwrap();
241        let report =
242            build_field(&source, &mut store, Limits::DEFAULT, BuildProfile::Runtime).unwrap();
243        store.sync().unwrap();
244
245        assert_eq!(report.profile, "runtime");
246        assert_eq!(report.candidate, "RAW");
247        assert_eq!(
248            report.candidates_evaluated, 1,
249            "a fixed profile must price exactly one candidate"
250        );
251        assert_eq!(report.source_len, source.len() as u64);
252
253        // Exactness: materialize the field root and byte-compare.
254        let field_id = report.ingest.field_id();
255        let field = crate::field::Field::open(&store, &field_id, Limits::DEFAULT).unwrap();
256        assert_eq!(field.materialize_exact(Limits::DEFAULT).unwrap(), source);
257    }
258
259    #[test]
260    fn stable_repeated_build_gives_identical_bytes_and_structure() {
261        let source = b"determinism is part of the contract".repeat(40);
262        let a = temp_root("det-a");
263        let b = temp_root("det-b");
264        let mut sa = FieldStore::open(&a).unwrap();
265        let mut sb = FieldStore::open(&b).unwrap();
266        let ra = build_field(&source, &mut sa, Limits::DEFAULT, BuildProfile::Runtime).unwrap();
267        let rb = build_field(&source, &mut sb, Limits::DEFAULT, BuildProfile::Runtime).unwrap();
268        assert_eq!(
269            ra.ingest.field_id(),
270            rb.ingest.field_id(),
271            "the same source must yield the same field id"
272        );
273    }
274
275    #[test]
276    fn profile_parse_rejects_unknown() {
277        assert_eq!(
278            BuildProfile::parse("runtime").unwrap(),
279            BuildProfile::Runtime
280        );
281        assert!(BuildProfile::parse("fastest").is_err());
282    }
283}