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