Skip to main content

vole_document/field/
dag.rs

1//! The procedural seed DAG: bounded closure traversal and materialization.
2//!
3//! Every node names a **bounded, versioned materializer**; there is no arbitrary
4//! execution (ADR-0025). A materializer is a pure function of the field's exact
5//! descriptor, the seed store, and the node's dependency outputs. Unknown kinds
6//! or materializer versions fail closed.
7//!
8//! Exact node kinds (`Q_ref`) resolve to byte-identical spans of the source via
9//! the *existing* partial-materialization machinery, so a narrow observation does
10//! not force a whole-document reconstruction. Derived node kinds (`Q_gen`, e.g.
11//! decoded streams, text runs, previews) are deterministic projections and are
12//! always labelled as such.
13
14use crate::container::{ObjectSource, ParsedDescriptor};
15use crate::error::{Error, Result};
16use crate::limits::Limits;
17use crate::materialize::observation::{select_ops, selection_references, serve_selection};
18use crate::store::{NodeId, SeedStore};
19
20use super::derive;
21use super::node::{NodeKind, SeedNode, read_object_params, read_span_params, read_u32_params};
22
23/// Supplies exact source byte ranges to a seed-DAG materialization.
24///
25/// The full implementation is a parsed descriptor
26/// ([`impl SourceServer for ParsedDescriptor`]); a partial loader that reads only
27/// the records a query needs is the other. Every materializer that resolves an
28/// exact `Q_ref` node goes through this one method, so the DAG logic cannot
29/// diverge between the complete and partial sources.
30pub trait SourceServer {
31    /// Bytes of the source range `[offset, offset + len)`.
32    fn serve_range(&self, offset: u64, len: u64, limits: Limits) -> Result<Vec<u8>>;
33    /// The whole reconstructed source (only a `DocumentExact` node needs this).
34    fn serve_document(&self, limits: Limits) -> Result<Vec<u8>>;
35
36    /// A *fine-grained* exact byte-range read: exactly `[offset, offset + len)`,
37    /// not rounded up to a coarse unit.
38    ///
39    /// This is the Phase-30.4 unit-addressed hook. The coarse lane is the only
40    /// source that implements it; every other source returns `Ok(None)`, so the
41    /// package/file model builders fall back to the whole-source path unchanged.
42    /// Reading through it is always permitted (a real read, always charged); it
43    /// is used to serve the few structural bytes a package reader consults — the
44    /// ZIP central directory and each member's local header — without
45    /// materializing the whole `DocumentExact` source.
46    fn read_exact(&self, _offset: u64, _len: u64, _limits: Limits) -> Result<Option<Vec<u8>>> {
47        Ok(None)
48    }
49
50    /// The source length, when this source can serve fine-grained reads.
51    fn exact_len(&self) -> Option<u64> {
52        None
53    }
54
55    /// The ZIP central-directory region `[cd_offset, source_len)` when this
56    /// source is unit-addressed over a ZIP package. `Ok(None)` otherwise.
57    fn zip_directory(&self, _limits: Limits) -> Result<Option<(u64, Vec<u8>)>> {
58        Ok(None)
59    }
60}
61
62impl SourceServer for ParsedDescriptor {
63    fn serve_range(&self, offset: u64, len: u64, limits: Limits) -> Result<Vec<u8>> {
64        serve_source_range(self, offset, len, limits)
65    }
66
67    fn serve_document(&self, limits: Limits) -> Result<Vec<u8>> {
68        crate::materialize::materialize(self, limits)
69    }
70}
71
72/// Hard cap on the number of nodes one materialization may evaluate.
73pub const MAX_EVAL_NODES: u64 = 1 << 20;
74/// Hard cap on total intermediate+output bytes one materialization may produce.
75pub const MAX_EVAL_BYTES: u64 = 1 << 32;
76
77/// A bounded evaluation budget, shared across a recursive materialization.
78#[derive(Debug, Clone)]
79pub struct EvalBudget {
80    /// Maximum nodes evaluated.
81    pub max_nodes: u64,
82    /// Maximum total bytes produced (intermediate + final).
83    pub max_bytes: u64,
84    /// Nodes evaluated so far.
85    pub nodes: u64,
86    /// Bytes produced so far.
87    pub produced: u64,
88}
89
90impl Default for EvalBudget {
91    fn default() -> Self {
92        EvalBudget {
93            max_nodes: MAX_EVAL_NODES,
94            max_bytes: MAX_EVAL_BYTES,
95            nodes: 0,
96            produced: 0,
97        }
98    }
99}
100
101impl EvalBudget {
102    fn charge_node(&mut self) -> Result<()> {
103        self.nodes = self
104            .nodes
105            .checked_add(1)
106            .ok_or_else(|| Error::resource_limit("seed evaluation node count overflow"))?;
107        if self.nodes > self.max_nodes {
108            return Err(Error::resource_limit(format!(
109                "seed evaluation exceeded {} nodes",
110                self.max_nodes
111            )));
112        }
113        Ok(())
114    }
115
116    pub(crate) fn charge_bytes(&mut self, n: u64) -> Result<()> {
117        self.produced = self
118            .produced
119            .checked_add(n)
120            .ok_or_else(|| Error::resource_limit("seed evaluation byte count overflow"))?;
121        if self.produced > self.max_bytes {
122            return Err(Error::resource_limit(format!(
123                "seed evaluation exceeded {} bytes",
124                self.max_bytes
125            )));
126        }
127        Ok(())
128    }
129}
130
131/// Load and decode one node from the store by id, verifying content identity.
132pub fn load_node(store: &dyn SeedStore, id: &NodeId) -> Result<SeedNode> {
133    let bytes = store.get_node(id)?;
134    let node = SeedNode::decode_canonical(&bytes)?;
135    if node.content_id() != *id {
136        return Err(Error::integrity_mismatch(format!(
137            "seed node {id} decoded to a different content id"
138        )));
139    }
140    node.check_limits(&Limits::DEFAULT)?;
141    Ok(node)
142}
143
144/// The dependency ids of a node's already-decoded canonical bytes.
145pub fn deps_of_canonical(bytes: &[u8]) -> Result<Vec<NodeId>> {
146    Ok(SeedNode::decode_canonical(bytes)?.deps)
147}
148
149/// Serve an exact source byte range `[offset, offset+len)` using the descriptor's
150/// program via the shared partial-materialization path. Does **not** materialize
151/// the whole document.
152fn serve_source_range(
153    parsed: &ParsedDescriptor,
154    offset: u64,
155    len: u64,
156    limits: Limits,
157) -> Result<Vec<u8>> {
158    let d = &parsed.descriptor;
159    if d.objects
160        .iter()
161        .any(|o| matches!(o, ObjectSource::External { .. }))
162    {
163        return Err(Error::unsupported_feature(
164            "field v1 requires an inline descriptor (no external objects)",
165        ));
166    }
167    let end = offset
168        .checked_add(len)
169        .ok_or_else(|| Error::usage("source slice end overflows"))?;
170    if end > d.source_len {
171        return Err(Error::usage(format!(
172            "source slice {offset}..{end} exceeds source length {}",
173            d.source_len
174        )));
175    }
176    let t_serve = crate::field::prof::start();
177    // Object lengths come straight off the object table; `ObjectSource::len` never
178    // resolves or copies bytes, so the window can be selected before any object is
179    // materialized. (External objects were rejected above, so every entry is inline.)
180    let object_lens: Vec<u64> = d.objects.iter().map(|o| o.len()).collect();
181    let channel_lens: Vec<u64> = d.channels.iter().map(|c| c.decoded_length).collect();
182    let t_select = crate::field::prof::start();
183    let window = select_ops(&d.program, &object_lens, &channel_lens, offset, end, limits)?;
184    let (objects_used, channels_used) =
185        selection_references(&window.ops, d.objects.len(), d.channels.len());
186    crate::field::prof::add_select(t_select);
187    // Bytes the selected window actually references: the invariant control axis.
188    // Neither branch below changes it, so it must not move between the variants.
189    let referenced_bytes: u64 = objects_used
190        .iter()
191        .enumerate()
192        .filter(|(_, used)| **used)
193        .map(|(id, _)| d.objects[id].len())
194        .sum();
195    let objects_used_n: u64 = objects_used.iter().filter(|u| **u).count() as u64;
196
197    // Phase 28.3 measurement prototype (feature `phase28-3-ablation`, off by
198    // default): choose between the eager full-table clone and a
199    // selection-before-materialization variant. The variant copies only the
200    // referenced objects and leaves the rest as never-dereferenced empty
201    // placeholders, exactly as the seek reader's partial loader already does.
202    #[cfg(feature = "phase28-3-ablation")]
203    let objects: Vec<Vec<u8>> = if super::selective_serve_enabled() {
204        let mut objs: Vec<Vec<u8>> = vec![Vec::new(); d.objects.len()];
205        for (id, used) in objects_used.iter().enumerate() {
206            if *used {
207                objs[id] = d.objects[id].as_inline().unwrap_or(&[]).to_vec();
208            }
209        }
210        objs
211    } else {
212        d.objects
213            .iter()
214            .map(|o| o.as_inline().unwrap_or(&[]).to_vec())
215            .collect()
216    };
217    #[cfg(not(feature = "phase28-3-ablation"))]
218    let objects: Vec<Vec<u8>> = d
219        .objects
220        .iter()
221        .map(|o| o.as_inline().unwrap_or(&[]).to_vec())
222        .collect();
223    let clone_bytes: u64 = objects.iter().map(|o| o.len() as u64).sum();
224
225    let served = serve_selection(
226        &objects,
227        &d.channels,
228        &d.models,
229        window,
230        &objects_used,
231        &channels_used,
232        offset,
233        end,
234        limits,
235    )?;
236    crate::field::prof::add_source_serve(
237        t_serve,
238        clone_bytes,
239        referenced_bytes,
240        d.objects.len() as u64,
241        objects_used_n,
242    );
243    Ok(served.bytes)
244}
245
246/// Which cache event a [`CacheNote`] reports (Phase 15.6).
247#[derive(Debug, Clone, Copy, PartialEq, Eq)]
248pub enum CacheEffect {
249    /// The node was served whole from the cache (its subtree was not traversed).
250    Hit,
251    /// The node's freshly computed output was written to the cache.
252    Stored,
253}
254
255/// A non-authoritative, best-effort cache event (Phase 15.6).
256///
257/// It never affects output bytes and is never consulted by
258/// `materialize`/`decode`/`verify`; a cache implementation may ignore it
259/// entirely. `wall_micros` is reserved for a measured per-node cost — the current
260/// call sites pass `0`, and the promotion policy falls back to a deterministic
261/// `bytes x kind_weight` estimate rather than a wall-clock reading (which would
262/// make promotion decisions irreproducible).
263#[derive(Debug, Clone, Copy)]
264pub struct CacheNote<'a> {
265    /// Whether the node was a hit or was just stored.
266    pub effect: CacheEffect,
267    /// What the node computes (drives the promotion cost weight).
268    pub kind: NodeKind,
269    /// The content-addressed node id.
270    pub id: &'a NodeId,
271    /// The node output length in bytes.
272    pub bytes: u64,
273    /// Reserved measured cost; `0` at the current call sites.
274    pub wall_micros: u64,
275}
276
277/// A node-output cache keyed by [`NodeId`]. Because a node's id binds its full
278/// dependency closure, an unchanged closure hits and a changed dependency misses;
279/// there is no invalidation pass (ADR-0025).
280///
281/// Implementations are disposable: [`materialize_node_cached`] treats any `get`
282/// error as a miss and never trusts bytes it cannot validate, so a corrupt cache
283/// causes recomputation rather than wrong output.
284pub trait OutputCache {
285    /// Fetch a cached node output, or `None` on a miss. A `get` error is treated
286    /// as a miss by the caller (the cache is disposable, never authority).
287    fn get(&self, id: &NodeId) -> Result<Option<Vec<u8>>>;
288    /// Store a node output. Best-effort: a `put` error does not fail the
289    /// materialization.
290    fn put(&mut self, id: &NodeId, bytes: &[u8]) -> Result<()>;
291    /// Phase 15.6. Advisory, best-effort reuse/store event, reported for every
292    /// cache hit and store. The default is a no-op, so `NoCache` and
293    /// `DerivedCache` are byte-identical to a build without this method; it is
294    /// never on the exactness path and can never change an output.
295    fn note(&mut self, _note: CacheNote<'_>) {}
296}
297
298/// A cache that stores nothing; used by the backward-compatible
299/// [`materialize_node`] wrapper and the `use_cache = false` cold court.
300#[derive(Debug, Clone, Copy, Default)]
301pub struct NoCache;
302
303impl OutputCache for NoCache {
304    fn get(&self, _id: &NodeId) -> Result<Option<Vec<u8>>> {
305        Ok(None)
306    }
307
308    fn put(&mut self, _id: &NodeId, _bytes: &[u8]) -> Result<()> {
309        Ok(())
310    }
311}
312
313/// Execution accounting for one materialization (ADR-0027). Reuse is claimed by
314/// an *execution counter*, never by wall-clock: `nodes_reused > 0` and a smaller
315/// `nodes_executed` are the evidence that persisted work was served from disk.
316#[derive(Debug, Clone, Copy, Default, PartialEq, Eq)]
317pub struct ReuseStats {
318    /// Nodes actually evaluated (cache misses).
319    pub nodes_executed: u64,
320    /// Nodes served whole from the cache (their subtrees were not traversed).
321    pub nodes_reused: u64,
322    /// Output bytes written to the cache during this materialization.
323    pub cache_bytes_written: u64,
324}
325
326/// The inverse work of one reconstruction, in abstract integer units (ADR-0034,
327/// plan §91). A unit is one node execution **or** one cold input byte the
328/// reconstruction had to read; it is never derived from the source size.
329///
330/// For a *cold* run (`use_cache = false`, or a cleared cache) `node_executions`
331/// is the run's `nodes_executed`; for a warm run the numerator comes from the
332/// difference the persisted store made.
333#[derive(Debug, Clone, Copy, Default, PartialEq, Eq)]
334pub struct InverseWork {
335    /// Node executions the run performed.
336    pub node_executions: u64,
337    /// Cold input bytes the run read (descriptor + manifest + index + seed).
338    pub input_bytes: u64,
339}
340
341impl InverseWork {
342    /// Assemble a receipt from the two independently measured integers.
343    pub const fn new(node_executions: u64, input_bytes: u64) -> Self {
344        InverseWork {
345            node_executions,
346            input_bytes,
347        }
348    }
349
350    /// Total work units: one per execution plus one per cold input byte.
351    pub const fn units(self) -> u64 {
352        self.node_executions.saturating_add(self.input_bytes)
353    }
354}
355
356/// `retained_inverse_work_fraction` (ADR-0034, plan §91):
357///
358/// ```text
359/// reused_persisted_inverse_work / total_inverse_work_required_by_cold_reconstruction
360/// ```
361///
362/// `cold` and `warm` are two receipts of the **same** query (cold = cache
363/// disabled or cleared; warm = the persisted store present). Work avoided is the
364/// drop in executions plus the drop in cold input bytes; the denominator is the
365/// cold run's total. Both are integer work units ([`InverseWork::units`]), so the
366/// fraction is derived from receipted integers, never from source size.
367///
368/// Returns `1.0` when the cold run required no work (an empty reconstruction),
369/// which is the honest limit rather than a fabricated ratio.
370pub fn retained_inverse_work_fraction(cold: InverseWork, warm: InverseWork) -> f64 {
371    let total = cold.units();
372    if total == 0 {
373        return 1.0;
374    }
375    let reused = cold
376        .node_executions
377        .saturating_sub(warm.node_executions)
378        .saturating_add(cold.input_bytes.saturating_sub(warm.input_bytes));
379    reused as f64 / total as f64
380}
381
382/// Materialize one node's output bytes, recursively resolving dependencies.
383pub fn materialize_node(
384    parsed: &ParsedDescriptor,
385    store: &dyn SeedStore,
386    node: &SeedNode,
387    limits: Limits,
388    budget: &mut EvalBudget,
389    depth: u16,
390) -> Result<Vec<u8>> {
391    let mut cache = NoCache;
392    let mut reuse = ReuseStats::default();
393    materialize_inner(
394        parsed, store, &mut cache, node, limits, budget, depth, &mut reuse,
395    )
396}
397
398/// Materialize one node's output bytes against an explicit [`SourceServer`].
399///
400/// This is the partial-reader entry point: the caller supplies a source that can
401/// serve ranges without being handed a fully parsed descriptor.
402pub fn materialize_node_with(
403    source: &dyn SourceServer,
404    store: &dyn SeedStore,
405    node: &SeedNode,
406    limits: Limits,
407    budget: &mut EvalBudget,
408    depth: u16,
409) -> Result<Vec<u8>> {
410    let mut cache = NoCache;
411    let mut reuse = ReuseStats::default();
412    materialize_inner(
413        source, store, &mut cache, node, limits, budget, depth, &mut reuse,
414    )
415}
416
417/// Materialize one node's output, consulting `cache` at **every** node (including
418/// dependencies).
419///
420/// A hit returns the cached bytes *without recursing into the node's dependency
421/// closure* and increments [`ReuseStats::nodes_reused`]; a miss evaluates the node
422/// (recursing through the same cache) and stores the output. Exact `Q_ref` nodes
423/// are cached too, since their output is equally a pure function of their id.
424#[allow(clippy::too_many_arguments)]
425pub fn materialize_node_cached(
426    parsed: &ParsedDescriptor,
427    store: &dyn SeedStore,
428    cache: &mut dyn OutputCache,
429    node: &SeedNode,
430    limits: Limits,
431    budget: &mut EvalBudget,
432    depth: u16,
433    reuse: &mut ReuseStats,
434) -> Result<Vec<u8>> {
435    materialize_inner(parsed, store, cache, node, limits, budget, depth, reuse)
436}
437
438/// The [`SourceServer`] counterpart of [`materialize_node_cached`].
439#[allow(clippy::too_many_arguments)]
440pub fn materialize_node_cached_with(
441    source: &dyn SourceServer,
442    store: &dyn SeedStore,
443    cache: &mut dyn OutputCache,
444    node: &SeedNode,
445    limits: Limits,
446    budget: &mut EvalBudget,
447    depth: u16,
448    reuse: &mut ReuseStats,
449) -> Result<Vec<u8>> {
450    materialize_inner(source, store, cache, node, limits, budget, depth, reuse)
451}
452
453#[allow(clippy::too_many_arguments)]
454fn materialize_inner(
455    source: &dyn SourceServer,
456    store: &dyn SeedStore,
457    cache: &mut dyn OutputCache,
458    node: &SeedNode,
459    limits: Limits,
460    budget: &mut EvalBudget,
461    depth: u16,
462    reuse: &mut ReuseStats,
463) -> Result<Vec<u8>> {
464    if depth == 0 {
465        return Err(Error::resource_limit("seed DAG exceeded its depth bound"));
466    }
467
468    let id = node.content_id();
469    // A hit is the whole subtree: return it without traversing dependencies. A
470    // cache error is a miss (the cache is disposable, never authority). Oversized
471    // cached bytes are likewise treated as a poisoned miss, not returned.
472    if let Ok(Some(bytes)) = cache.get(&id)
473        && bytes.len() as u64 <= node.limits.max_output_bytes
474    {
475        reuse.nodes_reused = reuse.nodes_reused.saturating_add(1);
476        budget.charge_bytes(bytes.len() as u64)?;
477        cache.note(CacheNote {
478            effect: CacheEffect::Hit,
479            kind: node.kind,
480            id: &id,
481            bytes: bytes.len() as u64,
482            wall_micros: 0,
483        });
484        return Ok(bytes);
485    }
486
487    budget.charge_node()?;
488    reuse.nodes_executed = reuse.nodes_executed.saturating_add(1);
489
490    let out = match node.kind {
491        NodeKind::DocumentExact => source.serve_document(limits)?,
492        NodeKind::SourceSlice | NodeKind::ResourceRef => {
493            let (offset, len) = read_span_params(&node.params)?;
494            source.serve_range(offset, len, limits)?
495        }
496        NodeKind::PdfRevision | NodeKind::PdfObject | NodeKind::PdfStreamEncoded => {
497            let (_number, _generation, extra) = read_object_params(&node.params)?;
498            // `extra` packs `offset` in the high 32 bits and `len` in the low 32.
499            let offset = extra >> 32;
500            let len = extra & 0xFFFF_FFFF;
501            source.serve_range(offset, len, limits)?
502        }
503        NodeKind::Concat | NodeKind::PageContent => {
504            let mut out = Vec::new();
505            for dep in &node.deps {
506                let child = load_node(store, dep)?;
507                let bytes = materialize_inner(
508                    source,
509                    store,
510                    cache,
511                    &child,
512                    limits,
513                    budget,
514                    depth - 1,
515                    reuse,
516                )?;
517                budget.charge_bytes(bytes.len() as u64)?;
518                out.extend_from_slice(&bytes);
519            }
520            out
521        }
522        NodeKind::Literal => node.params.clone(),
523        // The revision lineage is computed once at ingest from the byte-
524        // authoritative scan and held as its canonical JSON projection; the node
525        // is a leaf whose bytes are its `params` (Phase 17).
526        NodeKind::PdfRevisionLineage => node.params.clone(),
527        // A shared resource's canonical payload *is* its exact bytes; identity is
528        // content identity, so identical bytes across documents share this node.
529        NodeKind::ResourceBlob => node.params.clone(),
530        NodeKind::PackageRoot => source.serve_document(limits)?,
531        NodeKind::PackageMemberRaw => {
532            let (offset, len) = read_span_params(&node.params)?;
533            source.serve_range(offset, len, limits)?
534        }
535        NodeKind::PackageMemberDecoded => {
536            let (_ordinal, method, _extra) = read_object_params(&node.params)?;
537            let dep = node
538                .deps
539                .first()
540                .ok_or_else(|| Error::usage("PackageMemberDecoded has no dependency"))?;
541            let child = load_node(store, dep)?;
542            let encoded = materialize_inner(
543                source,
544                store,
545                cache,
546                &child,
547                limits,
548                budget,
549                depth - 1,
550                reuse,
551            )?;
552            match method {
553                // Stored (method 0): the raw span *is* the decoded bytes.
554                0 => {
555                    if encoded.len() as u64 != node.logical_output_len {
556                        return Err(Error::reconstruction_mismatch(format!(
557                            "stored member is {} bytes but the node declared {}",
558                            encoded.len(),
559                            node.logical_output_len
560                        )));
561                    }
562                    encoded
563                }
564                // Deflate (method 8): ZIP stores bare DEFLATE, not zlib-wrapped.
565                8 => derive::inflate_raw_deflate(&encoded, node.logical_output_len, limits)?,
566                // Any other method is a typed decline; the exact bytes are untouched.
567                other => {
568                    return Err(Error::unsupported_feature(format!(
569                        "zip member compression method {other} has no decoded representation"
570                    )));
571                }
572            }
573        }
574        NodeKind::PackageOpcModel => {
575            // The canonical OPC graph is derived on demand from the exact package
576            // source (the single dependency is the exact `PackageRoot`). XML parsing
577            // and all bounds live in `field::opc` / `adapter::package::opc`.
578            #[cfg(feature = "opc")]
579            {
580                // Unit-addressed lane (Phase 30.4): when the source can serve
581                // fine-grained ranges (the coarse lane), build the OPC model from
582                // the ZIP central directory plus only the members a package reader
583                // decodes, so the whole `DocumentExact` source is never materialized.
584                if let Some(bytes) = crate::field::pkgread::opc_model_from_units(source, limits)? {
585                    bytes
586                } else {
587                    let dep = node
588                        .deps
589                        .first()
590                        .ok_or_else(|| Error::usage("PackageOpcModel has no dependency"))?;
591                    let child = load_node(store, dep)?;
592                    let source = materialize_inner(
593                        source,
594                        store,
595                        cache,
596                        &child,
597                        limits,
598                        budget,
599                        depth - 1,
600                        reuse,
601                    )?;
602                    crate::field::opc::build_opc_model(&source, limits)?
603                }
604            }
605            #[cfg(not(feature = "opc"))]
606            {
607                return Err(Error::unsupported_feature(
608                    "OPC support is not compiled in (feature `opc`)",
609                ));
610            }
611        }
612        NodeKind::DocxModel => {
613            #[cfg(feature = "docx")]
614            {
615                let dep = node
616                    .deps
617                    .first()
618                    .ok_or_else(|| Error::usage("DocxModel has no dependency"))?;
619                let child = load_node(store, dep)?;
620                let opc_bytes = materialize_inner(
621                    source,
622                    store,
623                    cache,
624                    &child,
625                    limits,
626                    budget,
627                    depth - 1,
628                    reuse,
629                )?;
630                crate::adapter::docx::build_docx_model(&opc_bytes, limits)?
631            }
632            #[cfg(not(feature = "docx"))]
633            {
634                return Err(Error::unsupported_feature(
635                    "DOCX support is not compiled in (feature `docx`)",
636                ));
637            }
638        }
639        NodeKind::DocxStory => {
640            #[cfg(feature = "docx")]
641            {
642                let (story, part_name, profile) =
643                    crate::adapter::docx::read_story_params(&node.params)?;
644                let part_dep = node
645                    .deps
646                    .first()
647                    .ok_or_else(|| Error::usage("DocxStory has no part dependency"))?;
648                let part_node = load_node(store, part_dep)?;
649                let part_bytes = materialize_inner(
650                    source,
651                    store,
652                    cache,
653                    &part_node,
654                    limits,
655                    budget,
656                    depth - 1,
657                    reuse,
658                )?;
659                let styles = match node.deps.get(1) {
660                    Some(styles_dep) => {
661                        let styles_node = load_node(store, styles_dep)?;
662                        let styles_bytes = materialize_inner(
663                            source,
664                            store,
665                            cache,
666                            &styles_node,
667                            limits,
668                            budget,
669                            depth - 1,
670                            reuse,
671                        )?;
672                        Some(crate::adapter::docx::parse_styles(&styles_bytes, limits)?)
673                    }
674                    None => None,
675                };
676                crate::adapter::docx::wml::parse_story(
677                    &part_bytes,
678                    &part_name,
679                    story,
680                    &profile,
681                    styles.as_ref(),
682                    limits,
683                )?
684                .encode()
685            }
686            #[cfg(not(feature = "docx"))]
687            {
688                return Err(Error::unsupported_feature(
689                    "DOCX support is not compiled in (feature `docx`)",
690                ));
691            }
692        }
693        NodeKind::EpubModel => {
694            // The canonical EPUB (OCF) graph is derived on demand from the exact
695            // package source (the single dependency is the exact `PackageRoot`). It
696            // does not route through OPC. XML parsing and all bounds live in
697            // `adapter::epub`.
698            #[cfg(feature = "epub")]
699            {
700                let dep = node
701                    .deps
702                    .first()
703                    .ok_or_else(|| Error::usage("EpubModel has no dependency"))?;
704                let child = load_node(store, dep)?;
705                let source = materialize_inner(
706                    source,
707                    store,
708                    cache,
709                    &child,
710                    limits,
711                    budget,
712                    depth - 1,
713                    reuse,
714                )?;
715                crate::adapter::epub::build_epub_model(&source, limits)?
716            }
717            #[cfg(not(feature = "epub"))]
718            {
719                return Err(Error::unsupported_feature(
720                    "EPUB support is not compiled in (feature `epub`)",
721                ));
722            }
723        }
724        NodeKind::EpubContent => {
725            // One spine item's XHTML content document parsed into its bounded native
726            // model. Its single dependency is the decoded member node; the parse is
727            // bounded entirely inside `adapter::epub::content`. It never executes
728            // scripts and never fetches an external target.
729            #[cfg(feature = "epub")]
730            {
731                let (_spine, _ordinal, base_dir, _profile) =
732                    crate::adapter::epub::read_content_params(&node.params)?;
733                let dep = node
734                    .deps
735                    .first()
736                    .ok_or_else(|| Error::usage("EpubContent has no part dependency"))?;
737                let child = load_node(store, dep)?;
738                let bytes = materialize_inner(
739                    source,
740                    store,
741                    cache,
742                    &child,
743                    limits,
744                    budget,
745                    depth - 1,
746                    reuse,
747                )?;
748                crate::adapter::epub::parse_content(&bytes, &base_dir, limits)?.encode()
749            }
750            #[cfg(not(feature = "epub"))]
751            {
752                return Err(Error::unsupported_feature(
753                    "EPUB support is not compiled in (feature `epub`)",
754                ));
755            }
756        }
757        NodeKind::OdtModel => {
758            // The canonical ODT (ODF) graph is derived on demand from the exact
759            // package source (the single dependency is the exact `PackageRoot`). It
760            // does not route through OPC (ODF has no `[Content_Types].xml`). XML
761            // parsing and all bounds live in `adapter::odt`.
762            #[cfg(feature = "odt")]
763            {
764                // Unit-addressed lane (Phase 30.4): build the ODF discovery model
765                // from the ZIP central directory plus the decodable manifest members
766                // only, never materializing the whole `DocumentExact` source.
767                if let Some(bytes) = crate::field::pkgread::odt_model_from_units(source, limits)? {
768                    bytes
769                } else {
770                    let dep = node
771                        .deps
772                        .first()
773                        .ok_or_else(|| Error::usage("OdtModel has no dependency"))?;
774                    let child = load_node(store, dep)?;
775                    let source = materialize_inner(
776                        source,
777                        store,
778                        cache,
779                        &child,
780                        limits,
781                        budget,
782                        depth - 1,
783                        reuse,
784                    )?;
785                    crate::adapter::odt::build_odt_model(&source, limits)?
786                }
787            }
788            #[cfg(not(feature = "odt"))]
789            {
790                return Err(Error::unsupported_feature(
791                    "ODT support is not compiled in (feature `odt`)",
792                ));
793            }
794        }
795        NodeKind::OdtContent => {
796            // The OpenDocument main part (`content.xml`) parsed into its bounded
797            // native model. Its single dependency is the decoded member node; the
798            // parse is bounded entirely inside `adapter::odt`. No script execution,
799            // no remote fetch.
800            #[cfg(feature = "odt")]
801            {
802                let (_ordinal, part_name, profile) =
803                    crate::adapter::odt::read_content_params(&node.params)?;
804                let dep = node
805                    .deps
806                    .first()
807                    .ok_or_else(|| Error::usage("OdtContent has no part dependency"))?;
808                let child = load_node(store, dep)?;
809                let bytes = materialize_inner(
810                    source,
811                    store,
812                    cache,
813                    &child,
814                    limits,
815                    budget,
816                    depth - 1,
817                    reuse,
818                )?;
819                crate::adapter::odt::parse_content(&bytes, &part_name, &profile, limits)?.encode()
820            }
821            #[cfg(not(feature = "odt"))]
822            {
823                return Err(Error::unsupported_feature(
824                    "ODT support is not compiled in (feature `odt`)",
825                ));
826            }
827        }
828        NodeKind::OdsModel => {
829            // The canonical ODS (ODF spreadsheet) graph is derived on demand from the
830            // exact package source (the single dependency is the exact `PackageRoot`).
831            // It does not route through OPC (ODF has no `[Content_Types].xml`). XML
832            // parsing and all bounds live in `adapter::ods`.
833            #[cfg(feature = "ods")]
834            {
835                let dep = node
836                    .deps
837                    .first()
838                    .ok_or_else(|| Error::usage("OdsModel has no dependency"))?;
839                let child = load_node(store, dep)?;
840                let source = materialize_inner(
841                    source,
842                    store,
843                    cache,
844                    &child,
845                    limits,
846                    budget,
847                    depth - 1,
848                    reuse,
849                )?;
850                crate::adapter::ods::build_ods_model(&source, limits)?
851            }
852            #[cfg(not(feature = "ods"))]
853            {
854                return Err(Error::unsupported_feature(
855                    "ODS support is not compiled in (feature `ods`)",
856                ));
857            }
858        }
859        NodeKind::OdsContent => {
860            // The OpenDocument spreadsheet main part (`content.xml`) parsed into its
861            // bounded native model. Its single dependency is the decoded member node;
862            // the parse is bounded entirely inside `adapter::ods`. No script execution,
863            // no remote fetch.
864            #[cfg(feature = "ods")]
865            {
866                let (_ordinal, part_name, profile) =
867                    crate::adapter::ods::read_content_params(&node.params)?;
868                let dep = node
869                    .deps
870                    .first()
871                    .ok_or_else(|| Error::usage("OdsContent has no part dependency"))?;
872                let child = load_node(store, dep)?;
873                let bytes = materialize_inner(
874                    source,
875                    store,
876                    cache,
877                    &child,
878                    limits,
879                    budget,
880                    depth - 1,
881                    reuse,
882                )?;
883                crate::adapter::ods::parse_content(&bytes, &part_name, &profile, limits)?.encode()
884            }
885            #[cfg(not(feature = "ods"))]
886            {
887                return Err(Error::unsupported_feature(
888                    "ODS support is not compiled in (feature `ods`)",
889                ));
890            }
891        }
892        NodeKind::OdsStyles => {
893            // The OpenDocument styles part (`styles.xml`) parsed into its bounded
894            // cell-style/number-format model. Its single dependency is the decoded
895            // styles member node.
896            #[cfg(feature = "ods")]
897            {
898                let (_ordinal, part_name) = crate::adapter::ods::read_styles_params(&node.params)?;
899                let dep = node
900                    .deps
901                    .first()
902                    .ok_or_else(|| Error::usage("OdsStyles has no part dependency"))?;
903                let child = load_node(store, dep)?;
904                let bytes = materialize_inner(
905                    source,
906                    store,
907                    cache,
908                    &child,
909                    limits,
910                    budget,
911                    depth - 1,
912                    reuse,
913                )?;
914                crate::adapter::ods::parse_styles(&bytes, &part_name, limits)?.encode()
915            }
916            #[cfg(not(feature = "ods"))]
917            {
918                return Err(Error::unsupported_feature(
919                    "ODS support is not compiled in (feature `ods`)",
920                ));
921            }
922        }
923        NodeKind::OdpModel => {
924            // The canonical ODP (ODF presentation) graph is derived on demand from
925            // the exact package source (the single dependency is the exact
926            // `PackageRoot`). It does not route through OPC (ODF has no
927            // `[Content_Types].xml`). XML parsing and all bounds live in
928            // `adapter::odp`.
929            #[cfg(feature = "odp")]
930            {
931                let dep = node
932                    .deps
933                    .first()
934                    .ok_or_else(|| Error::usage("OdpModel has no dependency"))?;
935                let child = load_node(store, dep)?;
936                let source = materialize_inner(
937                    source,
938                    store,
939                    cache,
940                    &child,
941                    limits,
942                    budget,
943                    depth - 1,
944                    reuse,
945                )?;
946                crate::adapter::odp::build_odp_model(&source, limits)?
947            }
948            #[cfg(not(feature = "odp"))]
949            {
950                return Err(Error::unsupported_feature(
951                    "ODP support is not compiled in (feature `odp`)",
952                ));
953            }
954        }
955        NodeKind::OdpContent => {
956            // The OpenDocument presentation main part (`content.xml`) parsed into
957            // its bounded native model. Its single dependency is the decoded member
958            // node; the parse is bounded entirely inside `adapter::odp`. No script
959            // execution, no remote fetch.
960            #[cfg(feature = "odp")]
961            {
962                let (_ordinal, part_name, profile) =
963                    crate::adapter::odp::read_content_params(&node.params)?;
964                let dep = node
965                    .deps
966                    .first()
967                    .ok_or_else(|| Error::usage("OdpContent has no part dependency"))?;
968                let child = load_node(store, dep)?;
969                let bytes = materialize_inner(
970                    source,
971                    store,
972                    cache,
973                    &child,
974                    limits,
975                    budget,
976                    depth - 1,
977                    reuse,
978                )?;
979                crate::adapter::odp::parse_content(&bytes, &part_name, &profile, limits)?.encode()
980            }
981            #[cfg(not(feature = "odp"))]
982            {
983                return Err(Error::unsupported_feature(
984                    "ODP support is not compiled in (feature `odp`)",
985                ));
986            }
987        }
988        NodeKind::OdpStyles => {
989            // The OpenDocument styles part (`styles.xml`) parsed into its bounded
990            // style/master-page model. Its single dependency is the decoded styles
991            // member node.
992            #[cfg(feature = "odp")]
993            {
994                let (_ordinal, part_name) = crate::adapter::odp::read_styles_params(&node.params)?;
995                let dep = node
996                    .deps
997                    .first()
998                    .ok_or_else(|| Error::usage("OdpStyles has no part dependency"))?;
999                let child = load_node(store, dep)?;
1000                let bytes = materialize_inner(
1001                    source,
1002                    store,
1003                    cache,
1004                    &child,
1005                    limits,
1006                    budget,
1007                    depth - 1,
1008                    reuse,
1009                )?;
1010                crate::adapter::odp::parse_styles(&bytes, &part_name, limits)?.encode()
1011            }
1012            #[cfg(not(feature = "odp"))]
1013            {
1014                return Err(Error::unsupported_feature(
1015                    "ODP support is not compiled in (feature `odp`)",
1016                ));
1017            }
1018        }
1019        NodeKind::JsonModel => {
1020            // The canonical JSON structured-tree model is derived on demand from the
1021            // exact source. JSON has no package layer, so its single dependency is
1022            // the exact `DocumentExact` root (keyed by `sha256(source)` per
1023            // ADR-0060): the model node *reads the source bytes*, so it must carry a
1024            // source-identity input, never alias another field's source. Parsing and
1025            // all bounds live in `adapter::json`.
1026            #[cfg(feature = "json")]
1027            {
1028                let dep = node
1029                    .deps
1030                    .first()
1031                    .ok_or_else(|| Error::usage("JsonModel has no source dependency"))?;
1032                let child = load_node(store, dep)?;
1033                let source_bytes = materialize_inner(
1034                    source,
1035                    store,
1036                    cache,
1037                    &child,
1038                    limits,
1039                    budget,
1040                    depth - 1,
1041                    reuse,
1042                )?;
1043                crate::adapter::json::build_json_model(&source_bytes, limits)?
1044            }
1045            #[cfg(not(feature = "json"))]
1046            {
1047                return Err(Error::unsupported_feature(
1048                    "JSON support is not compiled in (feature `json`)",
1049                ));
1050            }
1051        }
1052        NodeKind::Json5Model => {
1053            // The canonical JSON5/JSONC structured-tree model is derived on demand
1054            // from the exact source. JSON5 has no package layer, so its single
1055            // dependency is the exact `DocumentExact` root (keyed by `sha256(source)`
1056            // per ADR-0060): the model node *reads the source bytes*, so it must
1057            // carry a source-identity input, never alias another field's source.
1058            // Parsing and all bounds live in `adapter::json5`.
1059            #[cfg(feature = "json5")]
1060            {
1061                let dep = node
1062                    .deps
1063                    .first()
1064                    .ok_or_else(|| Error::usage("Json5Model has no source dependency"))?;
1065                let child = load_node(store, dep)?;
1066                let source_bytes = materialize_inner(
1067                    source,
1068                    store,
1069                    cache,
1070                    &child,
1071                    limits,
1072                    budget,
1073                    depth - 1,
1074                    reuse,
1075                )?;
1076                crate::adapter::json5::build_json5_model(&source_bytes, limits)?
1077            }
1078            #[cfg(not(feature = "json5"))]
1079            {
1080                return Err(Error::unsupported_feature(
1081                    "JSON5 support is not compiled in (feature `json5`)",
1082                ));
1083            }
1084        }
1085        NodeKind::CborModel => {
1086            // The canonical CBOR structured-tree model is derived on demand from the
1087            // exact source. CBOR has no package layer, so its single dependency is the
1088            // exact `DocumentExact` root (keyed by `sha256(source)` per ADR-0060): the
1089            // model node *reads the source bytes*, so it must carry a source-identity
1090            // input, never alias another field's source. Parsing and all bounds live
1091            // in `adapter::cbor`.
1092            #[cfg(feature = "cbor")]
1093            {
1094                let dep = node
1095                    .deps
1096                    .first()
1097                    .ok_or_else(|| Error::usage("CborModel has no source dependency"))?;
1098                let child = load_node(store, dep)?;
1099                let source_bytes = materialize_inner(
1100                    source,
1101                    store,
1102                    cache,
1103                    &child,
1104                    limits,
1105                    budget,
1106                    depth - 1,
1107                    reuse,
1108                )?;
1109                crate::adapter::cbor::build_cbor_model(&source_bytes, limits)?
1110            }
1111            #[cfg(not(feature = "cbor"))]
1112            {
1113                return Err(Error::unsupported_feature(
1114                    "CBOR support is not compiled in (feature `cbor`)",
1115                ));
1116            }
1117        }
1118        NodeKind::MsgpackModel => {
1119            // The canonical MessagePack structured-tree model is derived on demand from
1120            // the exact source. MessagePack has no package layer, so its single
1121            // dependency is the exact `DocumentExact` root (keyed by `sha256(source)`
1122            // per ADR-0060): the model node *reads the source bytes*, so it must carry
1123            // a source-identity input, never alias another field's source. Parsing and
1124            // all bounds live in `adapter::msgpack`.
1125            #[cfg(feature = "msgpack")]
1126            {
1127                let dep = node
1128                    .deps
1129                    .first()
1130                    .ok_or_else(|| Error::usage("MsgpackModel has no source dependency"))?;
1131                let child = load_node(store, dep)?;
1132                let source_bytes = materialize_inner(
1133                    source,
1134                    store,
1135                    cache,
1136                    &child,
1137                    limits,
1138                    budget,
1139                    depth - 1,
1140                    reuse,
1141                )?;
1142                crate::adapter::msgpack::build_msgpack_model(&source_bytes, limits)?
1143            }
1144            #[cfg(not(feature = "msgpack"))]
1145            {
1146                return Err(Error::unsupported_feature(
1147                    "MessagePack support is not compiled in (feature `msgpack`)",
1148                ));
1149            }
1150        }
1151        NodeKind::ConfigModel => {
1152            // The canonical config-family model is derived on demand from the exact
1153            // source. The config family has no package layer, so its single dependency
1154            // is the exact `DocumentExact` root (keyed by `sha256(source)` per
1155            // ADR-0060): the model node *reads the source bytes*, so it must carry a
1156            // source-identity input, never alias another field's source. Parsing and
1157            // all bounds live in `adapter::config`.
1158            #[cfg(feature = "config")]
1159            {
1160                let dep = node
1161                    .deps
1162                    .first()
1163                    .ok_or_else(|| Error::usage("ConfigModel has no source dependency"))?;
1164                let child = load_node(store, dep)?;
1165                let source_bytes = materialize_inner(
1166                    source,
1167                    store,
1168                    cache,
1169                    &child,
1170                    limits,
1171                    budget,
1172                    depth - 1,
1173                    reuse,
1174                )?;
1175                crate::adapter::config::build_config_model(&source_bytes, limits)?
1176            }
1177            #[cfg(not(feature = "config"))]
1178            {
1179                return Err(Error::unsupported_feature(
1180                    "config support is not compiled in (feature `config`)",
1181                ));
1182            }
1183        }
1184        NodeKind::FeedModel => {
1185            // The canonical RSS/Atom feed model is derived on demand from the exact
1186            // source, via the shared bounded XML parser. A feed has no package layer,
1187            // so its single dependency is the exact `DocumentExact` root (keyed by
1188            // `sha256(source)` per ADR-0060): the model node *reads the source bytes*,
1189            // so it must carry a source-identity input, never alias another field's
1190            // source. Parsing and all bounds live in `adapter::feed`.
1191            #[cfg(feature = "feed")]
1192            {
1193                let dep = node
1194                    .deps
1195                    .first()
1196                    .ok_or_else(|| Error::usage("FeedModel has no source dependency"))?;
1197                let child = load_node(store, dep)?;
1198                let source_bytes = materialize_inner(
1199                    source,
1200                    store,
1201                    cache,
1202                    &child,
1203                    limits,
1204                    budget,
1205                    depth - 1,
1206                    reuse,
1207                )?;
1208                crate::adapter::feed::build_feed_model(&source_bytes, limits)?
1209            }
1210            #[cfg(not(feature = "feed"))]
1211            {
1212                return Err(Error::unsupported_feature(
1213                    "feed support is not compiled in (feature `feed`)",
1214                ));
1215            }
1216        }
1217        NodeKind::GeojsonModel => {
1218            // The canonical GeoJSON model is derived on demand from the exact source,
1219            // via the shared bounded JSON parser. GeoJSON has no package layer, so its
1220            // single dependency is the exact `DocumentExact` root (keyed by
1221            // `sha256(source)` per ADR-0060): the model node *reads the source bytes*,
1222            // so it must carry a source-identity input, never alias another field's
1223            // source. Parsing and all bounds live in `adapter::geojson`.
1224            #[cfg(feature = "geojson")]
1225            {
1226                let dep = node
1227                    .deps
1228                    .first()
1229                    .ok_or_else(|| Error::usage("GeojsonModel has no source dependency"))?;
1230                let child = load_node(store, dep)?;
1231                let source_bytes = materialize_inner(
1232                    source,
1233                    store,
1234                    cache,
1235                    &child,
1236                    limits,
1237                    budget,
1238                    depth - 1,
1239                    reuse,
1240                )?;
1241                crate::adapter::geojson::build_geojson_model(&source_bytes, limits)?
1242            }
1243            #[cfg(not(feature = "geojson"))]
1244            {
1245                return Err(Error::unsupported_feature(
1246                    "GeoJSON support is not compiled in (feature `geojson`)",
1247                ));
1248            }
1249        }
1250        NodeKind::GisModel => {
1251            // The canonical KML/GPX geospatial model is derived on demand from the
1252            // exact source, via the shared bounded XML parser. A GIS document has no
1253            // package layer, so its single dependency is the exact `DocumentExact`
1254            // root (keyed by `sha256(source)` per ADR-0060): the model node *reads the
1255            // source bytes*, so it must carry a source-identity input, never alias
1256            // another field's source. Parsing and all bounds live in `adapter::gis`.
1257            #[cfg(feature = "gis")]
1258            {
1259                let dep = node
1260                    .deps
1261                    .first()
1262                    .ok_or_else(|| Error::usage("GisModel has no source dependency"))?;
1263                let child = load_node(store, dep)?;
1264                let source_bytes = materialize_inner(
1265                    source,
1266                    store,
1267                    cache,
1268                    &child,
1269                    limits,
1270                    budget,
1271                    depth - 1,
1272                    reuse,
1273                )?;
1274                crate::adapter::gis::build_gis_model(&source_bytes, limits)?
1275            }
1276            #[cfg(not(feature = "gis"))]
1277            {
1278                return Err(Error::unsupported_feature(
1279                    "GIS support is not compiled in (feature `gis`)",
1280                ));
1281            }
1282        }
1283        NodeKind::NotebookModel => {
1284            // The canonical Jupyter notebook model is derived on demand from the exact
1285            // source, via the shared bounded JSON parser. A notebook has no package
1286            // layer, so its single dependency is the exact `DocumentExact` root (keyed by
1287            // `sha256(source)` per ADR-0060): the model node *reads the source bytes*,
1288            // so it must carry a source-identity input, never alias another field's
1289            // source. Parsing and all bounds live in `adapter::notebook`.
1290            #[cfg(feature = "notebook")]
1291            {
1292                let dep = node
1293                    .deps
1294                    .first()
1295                    .ok_or_else(|| Error::usage("NotebookModel has no source dependency"))?;
1296                let child = load_node(store, dep)?;
1297                let source_bytes = materialize_inner(
1298                    source,
1299                    store,
1300                    cache,
1301                    &child,
1302                    limits,
1303                    budget,
1304                    depth - 1,
1305                    reuse,
1306                )?;
1307                crate::adapter::notebook::build_notebook_model(&source_bytes, limits)?
1308            }
1309            #[cfg(not(feature = "notebook"))]
1310            {
1311                return Err(Error::unsupported_feature(
1312                    "notebook support is not compiled in (feature `notebook`)",
1313                ));
1314            }
1315        }
1316        NodeKind::FixedWidthModel => {
1317            // The canonical fixed-width model is derived on demand from the exact
1318            // source. Fixed-width has no package layer, so its single dependency is the
1319            // exact `DocumentExact` root (keyed by `sha256(source)` per ADR-0060): the
1320            // model node *reads the source bytes*, so it must carry a source-identity
1321            // input, never alias another field's source. Parsing and all bounds live in
1322            // `adapter::fixedwidth`.
1323            #[cfg(feature = "fixedwidth")]
1324            {
1325                let dep = node
1326                    .deps
1327                    .first()
1328                    .ok_or_else(|| Error::usage("FixedWidthModel has no source dependency"))?;
1329                let child = load_node(store, dep)?;
1330                let source_bytes = materialize_inner(
1331                    source,
1332                    store,
1333                    cache,
1334                    &child,
1335                    limits,
1336                    budget,
1337                    depth - 1,
1338                    reuse,
1339                )?;
1340                crate::adapter::fixedwidth::build_fixedwidth_model(&source_bytes, limits)?
1341            }
1342            #[cfg(not(feature = "fixedwidth"))]
1343            {
1344                return Err(Error::unsupported_feature(
1345                    "fixed-width support is not compiled in (feature `fixedwidth`)",
1346                ));
1347            }
1348        }
1349        NodeKind::RstModel => {
1350            // The canonical reStructuredText prose model is derived on demand from the
1351            // exact source. reStructuredText has no package layer, so its single
1352            // dependency is the exact `DocumentExact` root (keyed by `sha256(source)`
1353            // per ADR-0060): the model node *reads the source bytes*, so it must carry a
1354            // source-identity input, never alias another field's source. Parsing and all
1355            // bounds live in `adapter::rst`.
1356            #[cfg(feature = "rst")]
1357            {
1358                let dep = node
1359                    .deps
1360                    .first()
1361                    .ok_or_else(|| Error::usage("RstModel has no source dependency"))?;
1362                let child = load_node(store, dep)?;
1363                let source_bytes = materialize_inner(
1364                    source,
1365                    store,
1366                    cache,
1367                    &child,
1368                    limits,
1369                    budget,
1370                    depth - 1,
1371                    reuse,
1372                )?;
1373                crate::adapter::rst::build_rst_model(&source_bytes, limits)?
1374            }
1375            #[cfg(not(feature = "rst"))]
1376            {
1377                return Err(Error::unsupported_feature(
1378                    "reStructuredText support is not compiled in (feature `rst`)",
1379                ));
1380            }
1381        }
1382        NodeKind::AsciidocModel => {
1383            // The canonical AsciiDoc prose model is derived on demand from the exact
1384            // source. AsciiDoc has no package layer, so its single dependency is the
1385            // exact `DocumentExact` root (keyed by `sha256(source)` per ADR-0060): the
1386            // model node *reads the source bytes*, so it must carry a source-identity
1387            // input, never alias another field's source. Parsing and all bounds live in
1388            // `adapter::asciidoc`.
1389            #[cfg(feature = "asciidoc")]
1390            {
1391                let dep = node
1392                    .deps
1393                    .first()
1394                    .ok_or_else(|| Error::usage("AsciidocModel has no source dependency"))?;
1395                let child = load_node(store, dep)?;
1396                let source_bytes = materialize_inner(
1397                    source,
1398                    store,
1399                    cache,
1400                    &child,
1401                    limits,
1402                    budget,
1403                    depth - 1,
1404                    reuse,
1405                )?;
1406                crate::adapter::asciidoc::build_asciidoc_model(&source_bytes, limits)?
1407            }
1408            #[cfg(not(feature = "asciidoc"))]
1409            {
1410                return Err(Error::unsupported_feature(
1411                    "AsciiDoc support is not compiled in (feature `asciidoc`)",
1412                ));
1413            }
1414        }
1415        NodeKind::MdxModel => {
1416            // The canonical MDX (Markdown + JSX/ESM) model is derived on demand from
1417            // the exact source. MDX has no package layer, so its single dependency is
1418            // the exact `DocumentExact` root (keyed by `sha256(source)` per ADR-0060):
1419            // the model node *reads the source bytes*, so it must carry a
1420            // source-identity input, never alias another field's source. Parsing and
1421            // all bounds live in `adapter::mdx`.
1422            #[cfg(feature = "mdx")]
1423            {
1424                let dep = node
1425                    .deps
1426                    .first()
1427                    .ok_or_else(|| Error::usage("MdxModel has no source dependency"))?;
1428                let child = load_node(store, dep)?;
1429                let source_bytes = materialize_inner(
1430                    source,
1431                    store,
1432                    cache,
1433                    &child,
1434                    limits,
1435                    budget,
1436                    depth - 1,
1437                    reuse,
1438                )?;
1439                crate::adapter::mdx::build_mdx_model(&source_bytes, limits)?
1440            }
1441            #[cfg(not(feature = "mdx"))]
1442            {
1443                return Err(Error::unsupported_feature(
1444                    "MDX support is not compiled in (feature `mdx`)",
1445                ));
1446            }
1447        }
1448        NodeKind::MhtmlModel => {
1449            // The canonical MHTML (MIME HTML) model is derived on demand from the exact
1450            // source. MHTML has no package layer, so its single dependency is the exact
1451            // `DocumentExact` root (keyed by `sha256(source)` per ADR-0060): the model
1452            // node *reads the source bytes*, so it must carry a source-identity input,
1453            // never alias another field's source. Parsing and all bounds live in
1454            // `adapter::mhtml` (which reuses `adapter::eml` and `adapter::html`).
1455            #[cfg(feature = "mhtml")]
1456            {
1457                let dep = node
1458                    .deps
1459                    .first()
1460                    .ok_or_else(|| Error::usage("MhtmlModel has no source dependency"))?;
1461                let child = load_node(store, dep)?;
1462                let source_bytes = materialize_inner(
1463                    source,
1464                    store,
1465                    cache,
1466                    &child,
1467                    limits,
1468                    budget,
1469                    depth - 1,
1470                    reuse,
1471                )?;
1472                crate::adapter::mhtml::build_mhtml_model(&source_bytes, limits)?
1473            }
1474            #[cfg(not(feature = "mhtml"))]
1475            {
1476                return Err(Error::unsupported_feature(
1477                    "MHTML support is not compiled in (feature `mhtml`)",
1478                ));
1479            }
1480        }
1481        NodeKind::LogstreamModel => {
1482            // The canonical syslog / log-stream model is derived on demand from the
1483            // exact source. A log stream has no package layer, so its single dependency
1484            // is the exact `DocumentExact` root (keyed by `sha256(source)` per
1485            // ADR-0060): the model node *reads the source bytes*, so it must carry a
1486            // source-identity input, never alias another field's source. Parsing and
1487            // all bounds live in `adapter::logstream`.
1488            #[cfg(feature = "logstream")]
1489            {
1490                let dep = node
1491                    .deps
1492                    .first()
1493                    .ok_or_else(|| Error::usage("LogstreamModel has no source dependency"))?;
1494                let child = load_node(store, dep)?;
1495                let source_bytes = materialize_inner(
1496                    source,
1497                    store,
1498                    cache,
1499                    &child,
1500                    limits,
1501                    budget,
1502                    depth - 1,
1503                    reuse,
1504                )?;
1505                crate::adapter::logstream::build_logstream_model(&source_bytes, limits)?
1506            }
1507            #[cfg(not(feature = "logstream"))]
1508            {
1509                return Err(Error::unsupported_feature(
1510                    "log-stream support is not compiled in (feature `logstream`)",
1511                ));
1512            }
1513        }
1514        NodeKind::PkgmetaModel => {
1515            // The canonical package-metadata model is derived on demand from the exact
1516            // source. A manifest has no package layer, so its single dependency is the
1517            // exact `DocumentExact` root (keyed by `sha256(source)` per ADR-0060): the
1518            // model node *reads the source bytes*, so it must carry a source-identity
1519            // input, never alias another field's source. Parsing (which reuses the JSON
1520            // and TOML parsers) and all bounds live in `adapter::pkgmeta`.
1521            #[cfg(feature = "pkgmeta")]
1522            {
1523                let dep = node
1524                    .deps
1525                    .first()
1526                    .ok_or_else(|| Error::usage("PkgmetaModel has no source dependency"))?;
1527                let child = load_node(store, dep)?;
1528                let source_bytes = materialize_inner(
1529                    source,
1530                    store,
1531                    cache,
1532                    &child,
1533                    limits,
1534                    budget,
1535                    depth - 1,
1536                    reuse,
1537                )?;
1538                crate::adapter::pkgmeta::build_pkgmeta_model(&source_bytes, limits)?
1539            }
1540            #[cfg(not(feature = "pkgmeta"))]
1541            {
1542                return Err(Error::unsupported_feature(
1543                    "package-metadata support is not compiled in (feature `pkgmeta`)",
1544                ));
1545            }
1546        }
1547        NodeKind::ApispecModel => {
1548            // The canonical API/specification model is derived on demand from the exact
1549            // source, via the shared bounded JSON parser. An API spec has no package
1550            // layer, so its single dependency is the exact `DocumentExact` root (keyed
1551            // by `sha256(source)` per ADR-0060): the model node *reads the source
1552            // bytes*, so it must carry a source-identity input, never alias another
1553            // field's source. Parsing and all bounds live in `adapter::apispec`.
1554            #[cfg(feature = "apispec")]
1555            {
1556                let dep = node
1557                    .deps
1558                    .first()
1559                    .ok_or_else(|| Error::usage("ApispecModel has no source dependency"))?;
1560                let child = load_node(store, dep)?;
1561                let source_bytes = materialize_inner(
1562                    source,
1563                    store,
1564                    cache,
1565                    &child,
1566                    limits,
1567                    budget,
1568                    depth - 1,
1569                    reuse,
1570                )?;
1571                crate::adapter::apispec::build_apispec_model(&source_bytes, limits)?
1572            }
1573            #[cfg(not(feature = "apispec"))]
1574            {
1575                return Err(Error::unsupported_feature(
1576                    "API-specification support is not compiled in (feature `apispec`)",
1577                ));
1578            }
1579        }
1580        NodeKind::BibModel => {
1581            // The canonical bibliography model is derived on demand from the exact
1582            // source. A bibliography has no package layer, so its single dependency is
1583            // the exact `DocumentExact` root (keyed by `sha256(source)` per ADR-0060):
1584            // the model node *reads the source bytes*, so it must carry a source-identity
1585            // input, never alias another field's source. Parsing and all bounds live in
1586            // `adapter::bib`.
1587            #[cfg(feature = "bib")]
1588            {
1589                let dep = node
1590                    .deps
1591                    .first()
1592                    .ok_or_else(|| Error::usage("BibModel has no source dependency"))?;
1593                let child = load_node(store, dep)?;
1594                let source_bytes = materialize_inner(
1595                    source,
1596                    store,
1597                    cache,
1598                    &child,
1599                    limits,
1600                    budget,
1601                    depth - 1,
1602                    reuse,
1603                )?;
1604                crate::adapter::bib::build_bib_model(&source_bytes, limits)?
1605            }
1606            #[cfg(not(feature = "bib"))]
1607            {
1608                return Err(Error::unsupported_feature(
1609                    "bibliography support is not compiled in (feature `bib`)",
1610                ));
1611            }
1612        }
1613        NodeKind::RtfModel => {
1614            // The canonical RTF model is derived on demand from the exact source. RTF
1615            // has no package layer, so its single dependency is the exact
1616            // `DocumentExact` root (keyed by `sha256(source)` per ADR-0060): the model
1617            // node *reads the source bytes*, so it must carry a source-identity input,
1618            // never alias another field's source. Parsing and all bounds live in
1619            // `adapter::rtf`.
1620            #[cfg(feature = "rtf")]
1621            {
1622                let dep = node
1623                    .deps
1624                    .first()
1625                    .ok_or_else(|| Error::usage("RtfModel has no source dependency"))?;
1626                let child = load_node(store, dep)?;
1627                let source_bytes = materialize_inner(
1628                    source,
1629                    store,
1630                    cache,
1631                    &child,
1632                    limits,
1633                    budget,
1634                    depth - 1,
1635                    reuse,
1636                )?;
1637                crate::adapter::rtf::build_rtf_model(&source_bytes, limits)?
1638            }
1639            #[cfg(not(feature = "rtf"))]
1640            {
1641                return Err(Error::unsupported_feature(
1642                    "RTF support is not compiled in (feature `rtf`)",
1643                ));
1644            }
1645        }
1646        NodeKind::ContainerModel => {
1647            // The canonical archive-container model is derived on demand from the exact
1648            // source. A container has no separate package layer in this adapter, so its
1649            // single dependency is the exact source root (a `DocumentExact` for a TAR
1650            // tail, or the `PackageRoot` of the reused ZIP physical layer), keyed by
1651            // `sha256(source)` per ADR-0060: the model node *reads the source bytes*, so
1652            // it must carry a source-identity input, never alias another field's source.
1653            // Parsing and all bounds live in `adapter::container`.
1654            #[cfg(feature = "container")]
1655            {
1656                let dep = node
1657                    .deps
1658                    .first()
1659                    .ok_or_else(|| Error::usage("ContainerModel has no source dependency"))?;
1660                let child = load_node(store, dep)?;
1661                let source_bytes = materialize_inner(
1662                    source,
1663                    store,
1664                    cache,
1665                    &child,
1666                    limits,
1667                    budget,
1668                    depth - 1,
1669                    reuse,
1670                )?;
1671                crate::adapter::container::build_container_model(&source_bytes, limits)?
1672            }
1673            #[cfg(not(feature = "container"))]
1674            {
1675                return Err(Error::unsupported_feature(
1676                    "container support is not compiled in (feature `container`)",
1677                ));
1678            }
1679        }
1680        NodeKind::HdfModel => {
1681            // The canonical scientific-container model is derived on demand from the
1682            // exact source. Neither NetCDF classic nor the HDF5 subset has a package
1683            // layer, so its single dependency is the exact `DocumentExact` root (keyed
1684            // by `sha256(source)` per ADR-0060): the model node *reads the source
1685            // bytes*, so it must carry a source-identity input, never alias another
1686            // field's source. Parsing and all bounds live in `adapter::hdf`.
1687            #[cfg(feature = "hdf")]
1688            {
1689                let dep = node
1690                    .deps
1691                    .first()
1692                    .ok_or_else(|| Error::usage("HdfModel has no source dependency"))?;
1693                let child = load_node(store, dep)?;
1694                let source_bytes = materialize_inner(
1695                    source,
1696                    store,
1697                    cache,
1698                    &child,
1699                    limits,
1700                    budget,
1701                    depth - 1,
1702                    reuse,
1703                )?;
1704                crate::adapter::hdf::build_hdf_model(&source_bytes, limits)?
1705            }
1706            #[cfg(not(feature = "hdf"))]
1707            {
1708                return Err(Error::unsupported_feature(
1709                    "scientific-container support is not compiled in (feature `hdf`)",
1710                ));
1711            }
1712        }
1713        NodeKind::CfbfModel => {
1714            // The canonical CFBF/OLE2 compound-file model is derived on demand from the
1715            // exact source. A compound file has no package layer, so its single
1716            // dependency is the exact `DocumentExact` root (keyed by `sha256(source)`
1717            // per ADR-0060): the model node *reads the source bytes*, so it must carry a
1718            // source-identity input, never alias another field's source. Parsing and
1719            // all bounds live in `adapter::cfbf`.
1720            #[cfg(feature = "cfbf")]
1721            {
1722                let dep = node
1723                    .deps
1724                    .first()
1725                    .ok_or_else(|| Error::usage("CfbfModel has no source dependency"))?;
1726                let child = load_node(store, dep)?;
1727                let source_bytes = materialize_inner(
1728                    source,
1729                    store,
1730                    cache,
1731                    &child,
1732                    limits,
1733                    budget,
1734                    depth - 1,
1735                    reuse,
1736                )?;
1737                crate::adapter::cfbf::build_cfbf_model(&source_bytes, limits)?
1738            }
1739            #[cfg(not(feature = "cfbf"))]
1740            {
1741                return Err(Error::unsupported_feature(
1742                    "compound-file support is not compiled in (feature `cfbf`)",
1743                ));
1744            }
1745        }
1746        NodeKind::DocModel => {
1747            // The canonical legacy Word 97–2003 (`.doc`, MS-DOC) semantic model is
1748            // derived on demand from the exact source. A `.doc` has no package layer
1749            // beyond the reused CFBF physical layer, so its single dependency is the
1750            // exact `DocumentExact` root (keyed by `sha256(source)` per ADR-0060): the
1751            // model node *reads the source bytes*, so it must carry a source-identity
1752            // input, never alias another field's source. Parsing (on top of the CFBF
1753            // resolver) and all bounds live in `adapter::doc`.
1754            #[cfg(feature = "doc")]
1755            {
1756                let dep = node
1757                    .deps
1758                    .first()
1759                    .ok_or_else(|| Error::usage("DocModel has no source dependency"))?;
1760                let child = load_node(store, dep)?;
1761                let source_bytes = materialize_inner(
1762                    source,
1763                    store,
1764                    cache,
1765                    &child,
1766                    limits,
1767                    budget,
1768                    depth - 1,
1769                    reuse,
1770                )?;
1771                crate::adapter::doc::build_doc_model(&source_bytes, limits)?
1772            }
1773            #[cfg(not(feature = "doc"))]
1774            {
1775                return Err(Error::unsupported_feature(
1776                    "legacy Word support is not compiled in (feature `doc`)",
1777                ));
1778            }
1779        }
1780        NodeKind::XlsModel => {
1781            // The canonical legacy Excel 97–2003 (`.xls`, BIFF8) semantic model is
1782            // derived on demand from the exact source. A `.xls` has no package layer
1783            // beyond the reused CFBF physical layer, so its single dependency is the
1784            // exact `DocumentExact` root (keyed by `sha256(source)` per ADR-0060): the
1785            // model node *reads the source bytes*, so it must carry a source-identity
1786            // input, never alias another field's source. Parsing (on top of the CFBF
1787            // resolver) and all bounds live in `adapter::xls`.
1788            #[cfg(feature = "xls")]
1789            {
1790                let dep = node
1791                    .deps
1792                    .first()
1793                    .ok_or_else(|| Error::usage("XlsModel has no source dependency"))?;
1794                let child = load_node(store, dep)?;
1795                let source_bytes = materialize_inner(
1796                    source,
1797                    store,
1798                    cache,
1799                    &child,
1800                    limits,
1801                    budget,
1802                    depth - 1,
1803                    reuse,
1804                )?;
1805                crate::adapter::xls::build_xls_model(&source_bytes, limits)?
1806            }
1807            #[cfg(not(feature = "xls"))]
1808            {
1809                return Err(Error::unsupported_feature(
1810                    "legacy Excel support is not compiled in (feature `xls`)",
1811                ));
1812            }
1813        }
1814        NodeKind::PptModel => {
1815            // The canonical legacy PowerPoint 97–2003 (`.ppt`, MS-PPT) semantic model is
1816            // derived on demand from the exact source. A `.ppt` has no package layer
1817            // beyond the reused CFBF physical layer, so its single dependency is the
1818            // exact `DocumentExact` root (keyed by `sha256(source)` per ADR-0060): the
1819            // model node *reads the source bytes*, so it must carry a source-identity
1820            // input, never alias another field's source. Parsing (on top of the CFBF
1821            // resolver) and all bounds live in `adapter::ppt`.
1822            #[cfg(feature = "ppt")]
1823            {
1824                let dep = node
1825                    .deps
1826                    .first()
1827                    .ok_or_else(|| Error::usage("PptModel has no source dependency"))?;
1828                let child = load_node(store, dep)?;
1829                let source_bytes = materialize_inner(
1830                    source,
1831                    store,
1832                    cache,
1833                    &child,
1834                    limits,
1835                    budget,
1836                    depth - 1,
1837                    reuse,
1838                )?;
1839                crate::adapter::ppt::build_ppt_model(&source_bytes, limits)?
1840            }
1841            #[cfg(not(feature = "ppt"))]
1842            {
1843                return Err(Error::unsupported_feature(
1844                    "legacy PowerPoint support is not compiled in (feature `ppt`)",
1845                ));
1846            }
1847        }
1848        NodeKind::MsgModel => {
1849            // The canonical legacy Outlook message (`.msg`, MS-OXMSG) semantic model is
1850            // derived on demand from the exact source. A `.msg` has no package layer
1851            // beyond the reused CFBF physical layer, so its single dependency is the
1852            // exact `DocumentExact` root (keyed by `sha256(source)` per ADR-0060): the
1853            // model node *reads the source bytes*, so it must carry a source-identity
1854            // input, never alias another field's source. Parsing (on top of the CFBF
1855            // resolver) and all bounds live in `adapter::msg`.
1856            #[cfg(feature = "msg")]
1857            {
1858                let dep = node
1859                    .deps
1860                    .first()
1861                    .ok_or_else(|| Error::usage("MsgModel has no source dependency"))?;
1862                let child = load_node(store, dep)?;
1863                let source_bytes = materialize_inner(
1864                    source,
1865                    store,
1866                    cache,
1867                    &child,
1868                    limits,
1869                    budget,
1870                    depth - 1,
1871                    reuse,
1872                )?;
1873                crate::adapter::msg::build_msg_model(&source_bytes, limits)?
1874            }
1875            #[cfg(not(feature = "msg"))]
1876            {
1877                return Err(Error::unsupported_feature(
1878                    "legacy Outlook message support is not compiled in (feature `msg`)",
1879                ));
1880            }
1881        }
1882        NodeKind::YamlModel => {
1883            // The canonical YAML structured-tree model is derived on demand from the
1884            // exact source. YAML has no package layer, so its single dependency is
1885            // the exact `DocumentExact` root (keyed by `sha256(source)` per
1886            // ADR-0060): the model node *reads the source bytes*, so it must carry a
1887            // source-identity input, never alias another field's source. Parsing and
1888            // all bounds live in `adapter::yaml`.
1889            #[cfg(feature = "yaml")]
1890            {
1891                let dep = node
1892                    .deps
1893                    .first()
1894                    .ok_or_else(|| Error::usage("YamlModel has no source dependency"))?;
1895                let child = load_node(store, dep)?;
1896                let source_bytes = materialize_inner(
1897                    source,
1898                    store,
1899                    cache,
1900                    &child,
1901                    limits,
1902                    budget,
1903                    depth - 1,
1904                    reuse,
1905                )?;
1906                crate::adapter::yaml::build_yaml_model(&source_bytes, limits)?
1907            }
1908            #[cfg(not(feature = "yaml"))]
1909            {
1910                return Err(Error::unsupported_feature(
1911                    "YAML support is not compiled in (feature `yaml`)",
1912                ));
1913            }
1914        }
1915        NodeKind::CsvModel => {
1916            // The canonical CSV/TSV tabular model is derived on demand from the
1917            // exact source. CSV has no package layer, so its single dependency is
1918            // the exact `DocumentExact` root (keyed by `sha256(source)` per
1919            // ADR-0060): the model node *reads the source bytes*, so it must carry a
1920            // source-identity input, never alias another field's source. Parsing and
1921            // all bounds live in `adapter::csv`.
1922            #[cfg(feature = "csv")]
1923            {
1924                let dep = node
1925                    .deps
1926                    .first()
1927                    .ok_or_else(|| Error::usage("CsvModel has no source dependency"))?;
1928                let child = load_node(store, dep)?;
1929                let source_bytes = materialize_inner(
1930                    source,
1931                    store,
1932                    cache,
1933                    &child,
1934                    limits,
1935                    budget,
1936                    depth - 1,
1937                    reuse,
1938                )?;
1939                crate::adapter::csv::build_csv_model(&source_bytes, limits)?
1940            }
1941            #[cfg(not(feature = "csv"))]
1942            {
1943                return Err(Error::unsupported_feature(
1944                    "CSV support is not compiled in (feature `csv`)",
1945                ));
1946            }
1947        }
1948        NodeKind::MarkdownModel => {
1949            // The canonical Markdown prose model is derived on demand from the exact
1950            // source. Markdown has no package layer, so its single dependency is the
1951            // exact `DocumentExact` root (keyed by `sha256(source)` per ADR-0060): the
1952            // model node *reads the source bytes*, so it must carry a source-identity
1953            // input, never alias another field's source. Parsing and all bounds live
1954            // in `adapter::markdown`.
1955            #[cfg(feature = "markdown")]
1956            {
1957                let dep = node
1958                    .deps
1959                    .first()
1960                    .ok_or_else(|| Error::usage("MarkdownModel has no source dependency"))?;
1961                let child = load_node(store, dep)?;
1962                let source_bytes = materialize_inner(
1963                    source,
1964                    store,
1965                    cache,
1966                    &child,
1967                    limits,
1968                    budget,
1969                    depth - 1,
1970                    reuse,
1971                )?;
1972                crate::adapter::markdown::build_markdown_model(&source_bytes, limits)?
1973            }
1974            #[cfg(not(feature = "markdown"))]
1975            {
1976                return Err(Error::unsupported_feature(
1977                    "Markdown support is not compiled in (feature `markdown`)",
1978                ));
1979            }
1980        }
1981        NodeKind::XmlModel => {
1982            // The canonical XML structured-tree model is derived on demand from the
1983            // exact source. XML has no package layer, so its single dependency is the
1984            // exact `DocumentExact` root (keyed by `sha256(source)` per ADR-0060): the
1985            // model node *reads the source bytes*, so it must carry a source-identity
1986            // input, never alias another field's source. Parsing and all bounds live
1987            // in `adapter::xml`.
1988            #[cfg(feature = "xml")]
1989            {
1990                let dep = node
1991                    .deps
1992                    .first()
1993                    .ok_or_else(|| Error::usage("XmlModel has no source dependency"))?;
1994                let child = load_node(store, dep)?;
1995                let source_bytes = materialize_inner(
1996                    source,
1997                    store,
1998                    cache,
1999                    &child,
2000                    limits,
2001                    budget,
2002                    depth - 1,
2003                    reuse,
2004                )?;
2005                crate::adapter::xml::build_xml_model(&source_bytes, limits)?
2006            }
2007            #[cfg(not(feature = "xml"))]
2008            {
2009                return Err(Error::unsupported_feature(
2010                    "XML support is not compiled in (feature `xml`)",
2011                ));
2012            }
2013        }
2014        NodeKind::HtmlModel => {
2015            // The canonical HTML document model is derived on demand from the exact
2016            // source. HTML has no package layer, so its single dependency is the
2017            // exact `DocumentExact` root (keyed by `sha256(source)` per ADR-0060):
2018            // the model node *reads the source bytes*, so it must carry a
2019            // source-identity input, never alias another field's source. The parser
2020            // is bounded and error-recovering; all bounds live in `adapter::html`.
2021            #[cfg(feature = "html")]
2022            {
2023                let dep = node
2024                    .deps
2025                    .first()
2026                    .ok_or_else(|| Error::usage("HtmlModel has no source dependency"))?;
2027                let child = load_node(store, dep)?;
2028                let source_bytes = materialize_inner(
2029                    source,
2030                    store,
2031                    cache,
2032                    &child,
2033                    limits,
2034                    budget,
2035                    depth - 1,
2036                    reuse,
2037                )?;
2038                crate::adapter::html::build_html_model(&source_bytes, limits)?
2039            }
2040            #[cfg(not(feature = "html"))]
2041            {
2042                return Err(Error::unsupported_feature(
2043                    "HTML support is not compiled in (feature `html`)",
2044                ));
2045            }
2046        }
2047        NodeKind::TomlModel => {
2048            // The canonical TOML model is derived on demand from the exact source.
2049            // TOML has no package layer, so its single dependency is the exact
2050            // `DocumentExact` root (keyed by `sha256(source)` per ADR-0060): the
2051            // model node *reads the source bytes*, so it must carry a
2052            // source-identity input, never alias another field's source. All bounds
2053            // live in `adapter::toml`.
2054            #[cfg(feature = "toml")]
2055            {
2056                let dep = node
2057                    .deps
2058                    .first()
2059                    .ok_or_else(|| Error::usage("TomlModel has no source dependency"))?;
2060                let child = load_node(store, dep)?;
2061                let source_bytes = materialize_inner(
2062                    source,
2063                    store,
2064                    cache,
2065                    &child,
2066                    limits,
2067                    budget,
2068                    depth - 1,
2069                    reuse,
2070                )?;
2071                crate::adapter::toml::build_toml_model(&source_bytes, limits)?
2072            }
2073            #[cfg(not(feature = "toml"))]
2074            {
2075                return Err(Error::unsupported_feature(
2076                    "TOML support is not compiled in (feature `toml`)",
2077                ));
2078            }
2079        }
2080        NodeKind::JsonlModel => {
2081            // The canonical per-line JSONL model is derived on demand from the exact
2082            // source. JSONL has no package layer, so its single dependency is the
2083            // exact `DocumentExact` root (keyed by `sha256(source)` per ADR-0060): the
2084            // model node *reads the source bytes*, so it must carry a source-identity
2085            // input, never alias another field's source. Parsing and all bounds live
2086            // in `adapter::jsonl` (which reuses `adapter::json` per line).
2087            #[cfg(feature = "jsonl")]
2088            {
2089                let dep = node
2090                    .deps
2091                    .first()
2092                    .ok_or_else(|| Error::usage("JsonlModel has no source dependency"))?;
2093                let child = load_node(store, dep)?;
2094                let source_bytes = materialize_inner(
2095                    source,
2096                    store,
2097                    cache,
2098                    &child,
2099                    limits,
2100                    budget,
2101                    depth - 1,
2102                    reuse,
2103                )?;
2104                crate::adapter::jsonl::build_jsonl_model(&source_bytes, limits)?
2105            }
2106            #[cfg(not(feature = "jsonl"))]
2107            {
2108                return Err(Error::unsupported_feature(
2109                    "JSONL support is not compiled in (feature `jsonl`)",
2110                ));
2111            }
2112        }
2113        NodeKind::EmlModel => {
2114            // The canonical EML/MIME model is derived on demand from the exact source.
2115            // EML has no package layer, so its single dependency is the exact
2116            // `DocumentExact` root (keyed by `sha256(source)` per ADR-0060): the model
2117            // node *reads the source bytes*, so it must carry a source-identity input,
2118            // never alias another field's source. Parsing and all bounds live in
2119            // `adapter::eml`.
2120            #[cfg(feature = "eml")]
2121            {
2122                let dep = node
2123                    .deps
2124                    .first()
2125                    .ok_or_else(|| Error::usage("EmlModel has no source dependency"))?;
2126                let child = load_node(store, dep)?;
2127                let source_bytes = materialize_inner(
2128                    source,
2129                    store,
2130                    cache,
2131                    &child,
2132                    limits,
2133                    budget,
2134                    depth - 1,
2135                    reuse,
2136                )?;
2137                crate::adapter::eml::build_eml_model(&source_bytes, limits)?
2138            }
2139            #[cfg(not(feature = "eml"))]
2140            {
2141                return Err(Error::unsupported_feature(
2142                    "EML support is not compiled in (feature `eml`)",
2143                ));
2144            }
2145        }
2146        NodeKind::ParquetModel => {
2147            // The canonical Parquet model is derived on demand from the exact source.
2148            // Parquet has no package layer, so its single dependency is the exact
2149            // `DocumentExact` root (keyed by `sha256(source)` per ADR-0060): the model
2150            // node *reads the source bytes*, so it must carry a source-identity input,
2151            // never alias another field's source. Parsing and all bounds live in
2152            // `adapter::parquet`.
2153            #[cfg(feature = "parquet")]
2154            {
2155                let dep = node
2156                    .deps
2157                    .first()
2158                    .ok_or_else(|| Error::usage("ParquetModel has no source dependency"))?;
2159                let child = load_node(store, dep)?;
2160                let source_bytes = materialize_inner(
2161                    source,
2162                    store,
2163                    cache,
2164                    &child,
2165                    limits,
2166                    budget,
2167                    depth - 1,
2168                    reuse,
2169                )?;
2170                crate::adapter::parquet::build_parquet_model(&source_bytes, limits)?
2171            }
2172            #[cfg(not(feature = "parquet"))]
2173            {
2174                return Err(Error::unsupported_feature(
2175                    "Parquet support is not compiled in (feature `parquet`)",
2176                ));
2177            }
2178        }
2179        NodeKind::ArrowModel => {
2180            // The canonical Arrow IPC model is derived on demand from the exact
2181            // source. Arrow has no package layer, so its single dependency is the
2182            // exact `DocumentExact` root (keyed by `sha256(source)` per ADR-0060): the
2183            // model node *reads the source bytes*, so it must carry a source-identity
2184            // input, never alias another field's source. Parsing and all bounds live
2185            // in `adapter::arrow`.
2186            #[cfg(feature = "arrow")]
2187            {
2188                let dep = node
2189                    .deps
2190                    .first()
2191                    .ok_or_else(|| Error::usage("ArrowModel has no source dependency"))?;
2192                let child = load_node(store, dep)?;
2193                let source_bytes = materialize_inner(
2194                    source,
2195                    store,
2196                    cache,
2197                    &child,
2198                    limits,
2199                    budget,
2200                    depth - 1,
2201                    reuse,
2202                )?;
2203                crate::adapter::arrow::build_arrow_model(&source_bytes, limits)?
2204            }
2205            #[cfg(not(feature = "arrow"))]
2206            {
2207                return Err(Error::unsupported_feature(
2208                    "Arrow support is not compiled in (feature `arrow`)",
2209                ));
2210            }
2211        }
2212        NodeKind::XlsxModel => {
2213            // The canonical XLSX (SpreadsheetML) discovery model is derived on
2214            // demand from the canonical OPC model (the single dependency). XML
2215            // parsing and all bounds live in `adapter::xlsx`.
2216            #[cfg(feature = "xlsx")]
2217            {
2218                let dep = node
2219                    .deps
2220                    .first()
2221                    .ok_or_else(|| Error::usage("XlsxModel has no dependency"))?;
2222                let child = load_node(store, dep)?;
2223                let opc_bytes = materialize_inner(
2224                    source,
2225                    store,
2226                    cache,
2227                    &child,
2228                    limits,
2229                    budget,
2230                    depth - 1,
2231                    reuse,
2232                )?;
2233                crate::adapter::xlsx::build_xlsx_model(&opc_bytes, limits)?
2234            }
2235            #[cfg(not(feature = "xlsx"))]
2236            {
2237                return Err(Error::unsupported_feature(
2238                    "XLSX support is not compiled in (feature `xlsx`)",
2239                ));
2240            }
2241        }
2242        NodeKind::XlsxWorkbook => {
2243            // The parsed `xl/workbook.xml` sheet inventory. Its single dependency
2244            // is the decoded workbook member; the parse is bounded in
2245            // `adapter::xlsx`.
2246            #[cfg(feature = "xlsx")]
2247            {
2248                let (_ordinal, part_name) =
2249                    crate::adapter::xlsx::read_workbook_params(&node.params)?;
2250                let dep = node
2251                    .deps
2252                    .first()
2253                    .ok_or_else(|| Error::usage("XlsxWorkbook has no part dependency"))?;
2254                let child = load_node(store, dep)?;
2255                let bytes = materialize_inner(
2256                    source,
2257                    store,
2258                    cache,
2259                    &child,
2260                    limits,
2261                    budget,
2262                    depth - 1,
2263                    reuse,
2264                )?;
2265                let workbook = crate::adapter::xlsx::parse_workbook(&bytes, limits)?;
2266                let _ = part_name;
2267                workbook.encode()
2268            }
2269            #[cfg(not(feature = "xlsx"))]
2270            {
2271                return Err(Error::unsupported_feature(
2272                    "XLSX support is not compiled in (feature `xlsx`)",
2273                ));
2274            }
2275        }
2276        NodeKind::XlsxSheet => {
2277            // One worksheet parsed into its bounded cell model. Dependency 0 is
2278            // the decoded worksheet member; the optional dependency 1 is the
2279            // decoded shared-strings member, resolved into string cells.
2280            #[cfg(feature = "xlsx")]
2281            {
2282                let (_ordinal, shared_ordinal, _profile, part_name, sheet_name) =
2283                    crate::adapter::xlsx::read_sheet_params(&node.params)?;
2284                let part_dep = node
2285                    .deps
2286                    .first()
2287                    .ok_or_else(|| Error::usage("XlsxSheet has no part dependency"))?;
2288                let part_node = load_node(store, part_dep)?;
2289                let part_bytes = materialize_inner(
2290                    source,
2291                    store,
2292                    cache,
2293                    &part_node,
2294                    limits,
2295                    budget,
2296                    depth - 1,
2297                    reuse,
2298                )?;
2299                let shared = match (shared_ordinal, node.deps.get(1)) {
2300                    (Some(_), Some(shared_dep)) => {
2301                        let shared_node = load_node(store, shared_dep)?;
2302                        let shared_bytes = materialize_inner(
2303                            source,
2304                            store,
2305                            cache,
2306                            &shared_node,
2307                            limits,
2308                            budget,
2309                            depth - 1,
2310                            reuse,
2311                        )?;
2312                        Some(crate::adapter::xlsx::parse_shared_strings(
2313                            &shared_bytes,
2314                            limits,
2315                        )?)
2316                    }
2317                    _ => None,
2318                };
2319                crate::adapter::xlsx::parse_worksheet(
2320                    &part_bytes,
2321                    &part_name,
2322                    &sheet_name,
2323                    shared.as_deref(),
2324                    limits,
2325                )?
2326                .encode()
2327            }
2328            #[cfg(not(feature = "xlsx"))]
2329            {
2330                return Err(Error::unsupported_feature(
2331                    "XLSX support is not compiled in (feature `xlsx`)",
2332                ));
2333            }
2334        }
2335        NodeKind::PptxModel => {
2336            // The canonical PPTX (PresentationML) discovery model is derived on
2337            // demand from the canonical OPC model (the single dependency). XML
2338            // parsing and all bounds live in `adapter::pptx`.
2339            #[cfg(feature = "pptx")]
2340            {
2341                let dep = node
2342                    .deps
2343                    .first()
2344                    .ok_or_else(|| Error::usage("PptxModel has no dependency"))?;
2345                let child = load_node(store, dep)?;
2346                let opc_bytes = materialize_inner(
2347                    source,
2348                    store,
2349                    cache,
2350                    &child,
2351                    limits,
2352                    budget,
2353                    depth - 1,
2354                    reuse,
2355                )?;
2356                crate::adapter::pptx::build_pptx_model(&opc_bytes, limits)?
2357            }
2358            #[cfg(not(feature = "pptx"))]
2359            {
2360                return Err(Error::unsupported_feature(
2361                    "PPTX support is not compiled in (feature `pptx`)",
2362                ));
2363            }
2364        }
2365        NodeKind::PptxPresentation => {
2366            // The parsed `ppt/presentation.xml` slide inventory. Its single
2367            // dependency is the decoded presentation member; the parse is bounded
2368            // in `adapter::pptx`.
2369            #[cfg(feature = "pptx")]
2370            {
2371                let (_ordinal, part_name) =
2372                    crate::adapter::pptx::read_presentation_params(&node.params)?;
2373                let dep = node
2374                    .deps
2375                    .first()
2376                    .ok_or_else(|| Error::usage("PptxPresentation has no part dependency"))?;
2377                let child = load_node(store, dep)?;
2378                let bytes = materialize_inner(
2379                    source,
2380                    store,
2381                    cache,
2382                    &child,
2383                    limits,
2384                    budget,
2385                    depth - 1,
2386                    reuse,
2387                )?;
2388                let presentation = crate::adapter::pptx::parse_presentation(&bytes, limits)?;
2389                let _ = part_name;
2390                presentation.encode()
2391            }
2392            #[cfg(not(feature = "pptx"))]
2393            {
2394                return Err(Error::unsupported_feature(
2395                    "PPTX support is not compiled in (feature `pptx`)",
2396                ));
2397            }
2398        }
2399        NodeKind::PptxSlide => {
2400            // One slide parsed into its bounded shape model. Its single dependency
2401            // is the decoded slide member.
2402            #[cfg(feature = "pptx")]
2403            {
2404                let (_ordinal, profile, part_name) =
2405                    crate::adapter::pptx::read_slide_params(&node.params)?;
2406                let dep = node
2407                    .deps
2408                    .first()
2409                    .ok_or_else(|| Error::usage("PptxSlide has no part dependency"))?;
2410                let child = load_node(store, dep)?;
2411                let bytes = materialize_inner(
2412                    source,
2413                    store,
2414                    cache,
2415                    &child,
2416                    limits,
2417                    budget,
2418                    depth - 1,
2419                    reuse,
2420                )?;
2421                crate::adapter::pptx::parse_slide(&bytes, &part_name, &profile, limits)?.encode()
2422            }
2423            #[cfg(not(feature = "pptx"))]
2424            {
2425                return Err(Error::unsupported_feature(
2426                    "PPTX support is not compiled in (feature `pptx`)",
2427                ));
2428            }
2429        }
2430        NodeKind::PptxNotes => {
2431            // One notes-slide parsed into its bounded text model. Its single
2432            // dependency is the decoded notes member.
2433            #[cfg(feature = "pptx")]
2434            {
2435                let (_ordinal, profile, part_name) =
2436                    crate::adapter::pptx::read_notes_params(&node.params)?;
2437                let dep = node
2438                    .deps
2439                    .first()
2440                    .ok_or_else(|| Error::usage("PptxNotes has no part dependency"))?;
2441                let child = load_node(store, dep)?;
2442                let bytes = materialize_inner(
2443                    source,
2444                    store,
2445                    cache,
2446                    &child,
2447                    limits,
2448                    budget,
2449                    depth - 1,
2450                    reuse,
2451                )?;
2452                crate::adapter::pptx::parse_notes(&bytes, &part_name, &profile, limits)?.encode()
2453            }
2454            #[cfg(not(feature = "pptx"))]
2455            {
2456                return Err(Error::unsupported_feature(
2457                    "PPTX support is not compiled in (feature `pptx`)",
2458                ));
2459            }
2460        }
2461        NodeKind::PdfStreamDecoded => {
2462            let dep = node
2463                .deps
2464                .first()
2465                .ok_or_else(|| Error::usage("PdfStreamDecoded has no dependency"))?;
2466            let child = load_node(store, dep)?;
2467            let encoded = materialize_inner(
2468                source,
2469                store,
2470                cache,
2471                &child,
2472                limits,
2473                budget,
2474                depth - 1,
2475                reuse,
2476            )?;
2477            derive::inflate_zlib(&encoded, node.logical_output_len, limits)?
2478        }
2479        NodeKind::ContentOperators => {
2480            let dep = node
2481                .deps
2482                .first()
2483                .ok_or_else(|| Error::usage("ContentOperators has no dependency"))?;
2484            let child = load_node(store, dep)?;
2485            let decoded = materialize_inner(
2486                source,
2487                store,
2488                cache,
2489                &child,
2490                limits,
2491                budget,
2492                depth - 1,
2493                reuse,
2494            )?;
2495            derive::content_operators(&decoded, limits)?
2496        }
2497        NodeKind::TextRuns => {
2498            let dep = node
2499                .deps
2500                .first()
2501                .ok_or_else(|| Error::usage("TextRuns has no dependency"))?;
2502            let child = load_node(store, dep)?;
2503            let ops = materialize_inner(
2504                source,
2505                store,
2506                cache,
2507                &child,
2508                limits,
2509                budget,
2510                depth - 1,
2511                reuse,
2512            )?;
2513            derive::text_runs(&ops, limits)?
2514        }
2515        NodeKind::PagePreview => {
2516            let dep = node
2517                .deps
2518                .first()
2519                .ok_or_else(|| Error::usage("PagePreview has no dependency"))?;
2520            let child = load_node(store, dep)?;
2521            let content = materialize_inner(
2522                source,
2523                store,
2524                cache,
2525                &child,
2526                limits,
2527                budget,
2528                depth - 1,
2529                reuse,
2530            )?;
2531            let page = read_u32_params(&node.params)?;
2532            derive::page_preview(page, &content, limits)?
2533        }
2534    };
2535
2536    if out.len() as u64 > node.limits.max_output_bytes {
2537        return Err(Error::resource_limit(format!(
2538            "node {} produced {} bytes > its cap {}",
2539            node.kind.name(),
2540            out.len(),
2541            node.limits.max_output_bytes
2542        )));
2543    }
2544    budget.charge_bytes(out.len() as u64)?;
2545    // Best-effort persistence: a cache write failure never fails the observation.
2546    if cache.put(&id, &out).is_ok() {
2547        reuse.cache_bytes_written = reuse.cache_bytes_written.saturating_add(out.len() as u64);
2548        cache.note(CacheNote {
2549            effect: CacheEffect::Stored,
2550            kind: node.kind,
2551            id: &id,
2552            bytes: out.len() as u64,
2553            wall_micros: 0,
2554        });
2555    }
2556    Ok(out)
2557}
2558
2559#[cfg(test)]
2560mod tests {
2561    use super::*;
2562    use crate::container::ObjectSource;
2563    use crate::dra::{Op, Program};
2564    use crate::integrity::sha256;
2565    use crate::{EXACTNESS_PROFILE_EXACT_BYTES, SOURCE_FORMAT_OPAQUE};
2566
2567    fn parsed_for(source: &[u8]) -> ParsedDescriptor {
2568        let d = crate::container::Descriptor {
2569            universe: crate::container::UNIVERSE.to_string(),
2570            source_format: SOURCE_FORMAT_OPAQUE,
2571            format_basis: "opaque:test".to_string(),
2572            models: vec![],
2573            channels: vec![],
2574            objects: vec![ObjectSource::Inline(source.to_vec())],
2575            program: Program::new(vec![Op::EmitObject { object_id: 0 }]),
2576            observation_index: None,
2577            seek_directory: false,
2578            checkpoints: None,
2579            source_sha256: sha256(source),
2580            source_len: source.len() as u64,
2581        };
2582        let (bytes, _cost) = d.serialize().unwrap();
2583        let _ = EXACTNESS_PROFILE_EXACT_BYTES;
2584        crate::container::Descriptor::parse(&bytes, Limits::DEFAULT).unwrap()
2585    }
2586
2587    #[test]
2588    fn source_slice_matches_exact_bytes() {
2589        let parsed = parsed_for(b"hello field world");
2590        let mut budget = EvalBudget::default();
2591        let (off, len) = (6u64, 5u64);
2592        let node = SeedNode::new(
2593            NodeKind::SourceSlice,
2594            len,
2595            crate::field::node::span_params(off, len),
2596            vec![],
2597            "test",
2598        );
2599        let bytes = materialize_node(
2600            &parsed,
2601            &crate::store::FsSeedStore::open(
2602                std::env::temp_dir().join(format!("vole-dag-{}", std::process::id())),
2603            )
2604            .unwrap(),
2605            &node,
2606            Limits::DEFAULT,
2607            &mut budget,
2608            8,
2609        )
2610        .unwrap();
2611        assert_eq!(bytes, b"field");
2612    }
2613
2614    #[test]
2615    fn literal_roundtrips() {
2616        let parsed = parsed_for(b"x");
2617        let mut budget = EvalBudget::default();
2618        let node = SeedNode::new(NodeKind::Literal, 3, b"abc".to_vec(), vec![], "test");
2619        let bytes = materialize_node(
2620            &parsed,
2621            &crate::store::FsSeedStore::open(
2622                std::env::temp_dir().join(format!("vole-lit-{}", std::process::id())),
2623            )
2624            .unwrap(),
2625            &node,
2626            Limits::DEFAULT,
2627            &mut budget,
2628            4,
2629        )
2630        .unwrap();
2631        assert_eq!(bytes, b"abc");
2632    }
2633}