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}