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