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