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