Skip to main content

vole_document/field/
node.rs

1//! Canonical procedural seed nodes (Phase 11, ADR-0025).
2//!
3//! A [`SeedNode`] describes one **computation**, not one storage slot: a bounded,
4//! versioned materializer over zero or more dependencies, producing a declared
5//! output kind and logical length. Nodes are canonically encoded (little-endian,
6//! length-prefixed, no serde) so that the same computation always hashes to the
7//! same [`NodeId`] regardless of platform or insertion order.
8//!
9//! The node's `content_id` is **not** stored inside the node: it is
10//! `NodeId::of_node(canonical_bytes)`, which is what makes the graph
11//! content-addressed and immutable. Changing a dependency's bytes yields a
12//! different dependency id, hence a different node id; there is no mutation.
13
14use crate::error::{Error, Result};
15use crate::limits::Limits;
16use crate::store::NodeId;
17
18/// Canonical header byte for the node encoding.
19pub const NODE_MAGIC: u8 = 0xB1;
20/// Recommended maximum canonical node size (framing discipline).
21pub const MAX_NODE_BYTES: usize = 64 * 1024;
22/// Maximum dependency fanout of a single node.
23pub const MAX_NODE_DEPS: usize = 256;
24/// The materializer semantics version for v1.
25pub const MATERIALIZER_VERSION: u16 = 1;
26
27/// What a node computes. The numeric values are part of the canonical encoding.
28#[derive(Debug, Clone, Copy, PartialEq, Eq)]
29#[repr(u8)]
30pub enum NodeKind {
31    /// The whole exact source, materialized from the descriptor blob.
32    DocumentExact = 0x01,
33    /// An exact byte span of the source.
34    SourceSlice = 0x02,
35    /// An exact physical revision span.
36    PdfRevision = 0x03,
37    /// An exact indirect-object byte span (`n g obj … endobj`).
38    PdfObject = 0x04,
39    /// The exact encoded bytes of a stream object's data.
40    PdfStreamEncoded = 0x05,
41    /// The decoded (inflated) bytes of a lone `/FlateDecode` stream.
42    PdfStreamDecoded = 0x06,
43    /// A decoded content stream's operator token span.
44    ContentOperators = 0x07,
45    /// A deterministic text-run projection of content operators.
46    TextRuns = 0x08,
47    /// A page's concatenated decoded content stream.
48    PageContent = 0x09,
49    /// A deterministic structured page preview.
50    PagePreview = 0x0A,
51    /// A physical byte span of one resource object referenced by a page.
52    ResourceRef = 0x0B,
53    /// Concatenation of dependency outputs (exact).
54    Concat = 0x0C,
55    /// A raw exact literal held in the seed store.
56    Literal = 0x0D,
57    /// The whole exact package (ZIP/OCF/OPC) source, materialized through the
58    /// descriptor (`serve_document`). Exact (Phase 12.2).
59    PackageRoot = 0x0E,
60    /// One package member's exact raw compressed/stored span (a `SourceSlice`
61    /// with package provenance). Exact (Phase 12.2).
62    PackageMemberRaw = 0x0F,
63    /// One package member's decoded bytes: raw-DEFLATE inflate (method 8) or the
64    /// stored identity (method 0). Derived, never exact (Phase 12.2).
65    PackageMemberDecoded = 0x10,
66    /// The canonical generic-OPC package graph (content types, parts, package and
67    /// part relationships) derived on demand from the exact package source.
68    /// Derived, never exact (Phase 12.3).
69    PackageOpcModel = 0x11,
70    /// The canonical DOCX discovery model (main part, styles part, stories)
71    /// derived on demand from the canonical OPC model. Derived, never exact
72    /// (Phase 12.4).
73    DocxModel = 0x12,
74    /// One WordprocessingML story parsed into its canonical [`crate::adapter::docx::wml::StoryModel`],
75    /// honoring a declared extraction profile. Derived, never exact (Phase 12.4).
76    DocxStory = 0x13,
77    /// The canonical EPUB (OCF) discovery model: `mimetype` conformance facts,
78    /// container rootfiles, and the Package Document metadata/manifest/spine,
79    /// derived on demand from the exact package source. Derived, never exact
80    /// (Phase 12.5).
81    EpubModel = 0x14,
82    /// One spine item's XHTML content document parsed into its bounded native
83    /// content model (headings/paragraphs/lists/tables/links/resources), honoring
84    /// a declared extraction profile. Derived, never exact (Phase 12.6).
85    EpubContent = 0x15,
86    /// A byte-identical shareable resource (image/font/attachment) held inline and
87    /// addressed by **content identity** (Phase 12.8, ADR-0034). Its canonical
88    /// encoding embeds the exact bytes, so two documents carrying the same resource
89    /// share one node id (and one persisted blob) with no second identity scheme.
90    /// Exact (a resource's bytes are the source bytes).
91    ResourceBlob = 0x16,
92    /// The canonical ODT (ODF) discovery model: `mimetype` conformance facts and the
93    /// parsed `META-INF/manifest.xml` file entries with the main content part resolved
94    /// semantically, derived on demand from the exact package source. Derived, never
95    /// exact (Phase 13.3).
96    OdtModel = 0x17,
97    /// The OpenDocument main part (`content.xml`, `office:text`) parsed into its
98    /// bounded native content model (paragraphs/headings/spans/lists/tables/links/
99    /// bookmarks/notes/resources/tracked changes/sections), honoring a declared
100    /// extraction profile. Derived, never exact (Phase 13.3).
101    OdtContent = 0x18,
102    /// The PDF **revision lineage** (Phase 17): a compact JSON projection of the
103    /// physical incremental revision chain (count, ordered indices, byte spans,
104    /// resolved `startxref`/`/Prev`, and per-revision object/stream membership),
105    /// computed once at ingest from the byte-authoritative scan. Derived, never
106    /// exact. There is one document-level node (the whole lineage) and one
107    /// per-revision node.
108    PdfRevisionLineage = 0x19,
109    /// The canonical XLSX (SpreadsheetML) discovery model (Phase 21.1.1): the
110    /// workbook part (resolved via the `officeDocument` relationship and its
111    /// SpreadsheetML content type), the styles and shared-strings parts, and the
112    /// discovered worksheet parts. Derived on demand from the canonical OPC model.
113    /// Derived, never exact.
114    XlsxModel = 0x1A,
115    /// The parsed `xl/workbook.xml` sheet inventory (name, sheetId, r:id, state,
116    /// document order), parsed from the decoded workbook member. Derived, never
117    /// exact (Phase 21.1.1).
118    XlsxWorkbook = 0x1B,
119    /// One worksheet part (`xl/worksheets/sheetN.xml`) parsed into its bounded cell
120    /// model, honoring a declared extraction profile and resolving shared strings
121    /// from the decoded shared-strings member. Derived, never exact (Phase 21.1.1).
122    XlsxSheet = 0x1C,
123    /// The canonical PPTX (PresentationML) discovery model (Phase 21.2.1): the main
124    /// presentation part (resolved via the `officeDocument` relationship and its
125    /// PresentationML content type), the notes/slide masters, the layouts, themes
126    /// and media parts, and the discovered slide parts. Derived on demand from the
127    /// canonical OPC model. Derived, never exact.
128    PptxModel = 0x1D,
129    /// The parsed `ppt/presentation.xml` slide inventory (slide size and the
130    /// `p:sldIdLst` order), parsed from the decoded presentation member. Derived,
131    /// never exact (Phase 21.2.1).
132    PptxPresentation = 0x1E,
133    /// One slide part (`ppt/slides/slideN.xml`) parsed into its bounded shape model,
134    /// honoring a declared extraction profile. Derived, never exact (Phase 21.2.1).
135    PptxSlide = 0x1F,
136    /// One notes-slide part (`ppt/notesSlides/notesSlideN.xml`) parsed into its
137    /// bounded text model. Derived, never exact (Phase 21.2.1).
138    PptxNotes = 0x20,
139    /// The canonical ODS (ODF spreadsheet) discovery model: `mimetype` conformance
140    /// facts and the parsed `META-INF/manifest.xml` file entries with the main
141    /// content part resolved semantically, derived on demand from the exact package
142    /// source. Derived, never exact (Phase 21.3.1).
143    OdsModel = 0x21,
144    /// The OpenDocument spreadsheet main part (`content.xml`, `office:spreadsheet`)
145    /// parsed into its bounded native model (sheets/rows/cells with typed values,
146    /// stored formulas, cell styles, named expressions, and annotations), honoring a
147    /// declared extraction profile. Derived, never exact (Phase 21.3.1).
148    OdsContent = 0x22,
149    /// The OpenDocument styles part (`styles.xml`) parsed into its bounded cell-style
150    /// and number-format model. Derived, never exact (Phase 21.3.1).
151    OdsStyles = 0x23,
152    /// The canonical ODP (ODF presentation) discovery model: `mimetype` conformance
153    /// facts and the parsed `META-INF/manifest.xml` file entries with the main
154    /// content part and the `Pictures/*` media parts resolved semantically, derived
155    /// on demand from the exact package source. Derived, never exact (Phase 21.4.1).
156    OdpModel = 0x24,
157    /// The OpenDocument presentation main part (`content.xml`, `office:presentation`)
158    /// parsed into its bounded native model (`draw:page` slides in document order,
159    /// shapes/text runs, embedded tables, notes, styles, and image references),
160    /// honoring a declared extraction profile. Derived, never exact (Phase 21.4.1).
161    OdpContent = 0x25,
162    /// The OpenDocument styles part (`styles.xml`) parsed into its bounded style and
163    /// master-page model. Derived, never exact (Phase 21.4.1).
164    OdpStyles = 0x26,
165    /// The canonical, representation-preserving JSON structured-tree model
166    /// (Phase 21.5.1): every token's exact source span, member order, duplicate
167    /// keys, and token spelling. It is derived on demand from the exact source
168    /// (its single dependency is the `DocumentExact` root, keyed by
169    /// `sha256(source)` per ADR-0060). JSON has no package layer. Derived, never
170    /// exact.
171    JsonModel = 0x27,
172    /// The canonical, representation-preserving YAML structured-tree model
173    /// (Phase 21.6.1): every node's exact source span and kind/style, the anchored
174    /// graph (anchors preserved, aliases never expanded), literal tags, scalar
175    /// styles, ordered documents, merge keys, and comment spans. It is derived on
176    /// demand from the exact source (its single dependency is the `DocumentExact`
177    /// root, keyed by `sha256(source)` per ADR-0060). YAML has no package layer.
178    /// Derived, never exact.
179    YamlModel = 0x28,
180    /// The canonical, representation-preserving CSV/TSV tabular model
181    /// (Phase 21.7.1): every record's and field's exact source span, the recorded
182    /// dialect (delimiter/quote/line terminator/BOM), original quoting, and the
183    /// header row. It is derived on demand from the exact source (its single
184    /// dependency is the `DocumentExact` root, keyed by `sha256(source)` per
185    /// ADR-0060). CSV/TSV has no package layer. Derived, never exact.
186    CsvModel = 0x29,
187    /// The canonical, representation-preserving Markdown prose model (Phase
188    /// 21.8.1): every block's and inline span's exact source span, ATX headings and
189    /// their levels, paragraphs, ordered/unordered lists, fenced (language-tagged)
190    /// and indented code blocks, blockquotes, tables, links/images with targets and
191    /// titles, reference definitions, footnotes, and front matter. It is derived on
192    /// demand from the exact source (its single dependency is the `DocumentExact`
193    /// root, keyed by `sha256(source)` per ADR-0060). Markdown has no package layer.
194    /// Derived, never exact.
195    MarkdownModel = 0x2A,
196    /// The canonical, representation-preserving XML structured-tree model
197    /// (Phase 21.9): every element's qualified-name/start-tag/end-tag/full span,
198    /// every attribute's name/quoted-value/inner-value/full span, every text run,
199    /// CDATA section, comment, processing instruction, DOCTYPE, and namespace
200    /// declaration, all in document order. Entity references are surfaced literally
201    /// (never expanded) and no DTD internal subset is processed. It is derived on
202    /// demand from the exact source (its single dependency is the `DocumentExact`
203    /// root, keyed by `sha256(source)` per ADR-0060). XML has no package layer.
204    /// Derived, never exact.
205    XmlModel = 0x2B,
206    /// The canonical, representation-preserving HTML document model (Phase 21.10):
207    /// every element's qualified-name/start-tag/end-tag/full span, every attribute's
208    /// name/quoted-value/inner-value/full span (quoted, single-quoted, unquoted, or
209    /// boolean), every text run, comment, DOCTYPE, and raw `<script>`/`<style>`
210    /// content, all in document order. The parser is bounded and
211    /// error-recovering (implicit tag closing, void elements, stray end tags);
212    /// entity references are surfaced literally (never expanded) and `script`/`style`
213    /// content is captured as raw bytes (never executed). It is derived on demand
214    /// from the exact source (its single dependency is the `DocumentExact` root,
215    /// keyed by `sha256(source)` per ADR-0060). HTML has no package layer. Derived,
216    /// never exact.
217    HtmlModel = 0x2C,
218    /// The canonical, representation-preserving TOML model (Phase 21.11): every
219    /// table (`[a.b]`), array of tables (`[[a]]`), inline table, array, key, value,
220    /// and comment with its exact source span; dotted keys build the tables they
221    /// name; every scalar keeps its **exact spelling** (strings, integers, floats
222    /// incl. `inf`/`nan`, booleans, and date-times). TOML's duplicate-key /
223    /// redefinition rules are enforced. It is derived on demand from the exact
224    /// source (its single dependency is the `DocumentExact` root, keyed by
225    /// `sha256(source)` per ADR-0060). TOML has no package layer. Derived, never
226    /// exact.
227    TomlModel = 0x2D,
228    /// The canonical, per-line JSONL/NDJSON model (Phase 21.12): every non-blank
229    /// record's **exact source line span** and terminator, and its parse by the
230    /// shared JSON parser (so each record's member order, duplicate keys, numeric
231    /// and escape spelling, and exact token spans are preserved). It is derived on
232    /// demand from the exact source (its single dependency is the `DocumentExact`
233    /// root, keyed by `sha256(source)` per ADR-0060). JSONL has no package layer.
234    /// Derived, never exact.
235    JsonlModel = 0x2E,
236    /// The canonical, representation-preserving EML/MIME message model (Phase
237    /// 21.13): every header's exact name/value/full span and its order, duplicate
238    /// headers kept distinct, the resolved `multipart/*` tree, nested
239    /// `message/rfc822`, and the decoded `Content-Transfer-Encoding` constituents. It
240    /// is derived on demand from the exact source (its single dependency is the
241    /// `DocumentExact` root, keyed by `sha256(source)` per ADR-0060). EML has no
242    /// package layer. Derived, never exact.
243    EmlModel = 0x2F,
244    /// The canonical, bounded Parquet model (Phase 21.14): the parsed Thrift-Compact
245    /// footer inventory — the flattened schema (names, physical/logical types,
246    /// repetition), the logical leaf columns, and the row-group/column-chunk
247    /// descriptors, each chunk with its exact source span and statistics. It is
248    /// derived on demand from the exact source (its single dependency is the
249    /// `DocumentExact` root, keyed by `sha256(source)` per ADR-0060). Parquet has no
250    /// package layer. Derived, never exact.
251    ParquetModel = 0x30,
252    /// The canonical, bounded Arrow IPC model (Phase 21.16): the parsed Flatbuffers
253    /// footer (file format) or leading schema message (stream format) inventory —
254    /// the flattened schema (names, type tags/parameters, nullability, children),
255    /// the decodable top-level columns with their pre-computed buffer slots, and the
256    /// record-batch descriptors (each with its exact source span and its declared
257    /// node/buffer counts). It is derived on demand from the exact source (its
258    /// single dependency is the `DocumentExact` root, keyed by `sha256(source)` per
259    /// ADR-0060). Arrow has no package layer. Derived, never exact.
260    ArrowModel = 0x31,
261    /// The canonical, representation-preserving JSON5 / JSONC structured-tree model
262    /// (Phase 21.17.1): the same representation guarantees as [`NodeKind::JsonModel`]
263    /// (exact token spans, member order, duplicate keys, numeric/escape spelling)
264    /// plus the JSON5 superset — unquoted keys, single-quoted strings, trailing
265    /// commas, hex/leading-dot/`Infinity`/`NaN` numbers, string continuations, the
266    /// extended whitespace set, and every comment's exact span — and a recorded
267    /// dialect (`jsonc` vs `json5`). It is derived on demand from the exact source
268    /// (its single dependency is the `DocumentExact` root, keyed by `sha256(source)`
269    /// per ADR-0060). JSON5 has no package layer. Derived, never exact.
270    Json5Model = 0x32,
271    /// The canonical, representation-preserving CBOR structured-tree model (Phase
272    /// 21.18): the same representation guarantees as [`NodeKind::JsonModel`] (exact
273    /// token spans, member order, duplicate keys) plus the CBOR-specific facts — the
274    /// major type of every item, the encoding width actually used (the head byte's
275    /// additional-information nibble), byte-vs-text string as distinct kinds, tag
276    /// numbers preserved verbatim (never resolved/expanded), float width
277    /// (half/single/double) with the IEEE-754 bits, simple values, and definite vs
278    /// indefinite-length items. It is derived on demand from the exact source (its
279    /// single dependency is the `DocumentExact` root, keyed by `sha256(source)` per
280    /// ADR-0060). CBOR has no package layer. Derived, never exact.
281    CborModel = 0x33,
282    /// The canonical, representation-preserving MessagePack structured-tree model
283    /// (Phase 21.19): the same representation guarantees as [`NodeKind::JsonModel`]
284    /// (exact token spans, member order, duplicate keys) plus the MessagePack-specific
285    /// facts — the exact format byte of every item (so the encoding width **and
286    /// signedness** actually used are preserved), `str` vs `bin` as distinct kinds,
287    /// float width (`float32`/`float64`) with the IEEE-754 bits, and extension type
288    /// numbers + payload lengths preserved verbatim (never interpreted). It is derived
289    /// on demand from the exact source (its single dependency is the `DocumentExact`
290    /// root, keyed by `sha256(source)` per ADR-0060). MessagePack has no package layer.
291    /// Derived, never exact.
292    MsgpackModel = 0x34,
293    /// The canonical, representation-preserving config-family model (Phase 21.20):
294    /// the whole source parsed as INI / `.env` / Java `.properties` into a bounded
295    /// arena of lines (exact content spans and terminators) with exact key,
296    /// separator, and value spans per entry, the recorded dialect, the `export`
297    /// marker, quoting and inline-comment facts, `properties` trailing-`\`
298    /// continuations, and `\uXXXX` escapes preserved as spelling. It is computed on
299    /// demand from the exact source (its single dependency is the `DocumentExact`
300    /// root, keyed by `sha256(source)` per ADR-0060); the config family has no
301    /// package layer. Derived, never exact.
302    ConfigModel = 0x35,
303    /// The canonical, representation-preserving RSS 2.0 / Atom 1.0 feed model
304    /// (Phase 21.21): the whole source parsed (via the shared bounded XML parser)
305    /// into a span-preserving element/attribute tree plus the recorded dialect
306    /// (`rss`/`atom`), the channel/feed container node, and the ordered `<item>` /
307    /// `<entry>` record nodes. Every element's and attribute's exact source span,
308    /// element order, attribute spelling (Atom `<link href=… rel=…>`, RSS `<guid
309    /// isPermaLink=…>`), and the Atom namespace declaration are preserved; entity
310    /// references are surfaced literally (never expanded). It is computed on demand
311    /// from the exact source (its single dependency is the `DocumentExact` root,
312    /// keyed by `sha256(source)` per ADR-0060); a feed has no package layer. Derived,
313    /// never exact.
314    FeedModel = 0x36,
315    /// The canonical, representation-preserving GeoJSON (RFC 7946) model
316    /// (Phase 21.22): the whole source parsed by the **shared JSON parser** into a
317    /// span-preserving JSON arena (exact token spans, member order, duplicate keys,
318    /// numeric/escape spelling) plus the GeoJSON semantic anchors — the recorded
319    /// root class (one of the nine RFC 7946 type names), the exact `"type"` token,
320    /// and the geometry and feature object nodes in document order. `coordinates`
321    /// nesting is preserved verbatim (a position is `[lon, lat, (alt)]`, never
322    /// normalized/reordered), and `properties`/`id`/`bbox`/`geometry`/`features`
323    /// order plus every foreign member are preserved. It is computed on demand from
324    /// the exact source (its single dependency is the `DocumentExact` root, keyed by
325    /// `sha256(source)` per ADR-0060); GeoJSON has no package layer. Derived, never
326    /// exact.
327    GeojsonModel = 0x37,
328    /// The canonical, representation-preserving KML 2.2 / GPX 1.1 geospatial model
329    /// (Phase 21.23): the whole source parsed (via the shared bounded XML parser)
330    /// into a span-preserving element/attribute tree plus the recorded dialect
331    /// (`kml`/`gpx`), the ordered record nodes (KML `<Placemark>` features, or GPX
332    /// top-level `<wpt>`/`<rte>`/`<trk>` records), and the ordered point nodes (KML
333    /// `Point`/`LineString`/`Polygon` geometry, or GPX `<wpt>`/`<rtept>`/`<trkpt>`
334    /// point elements). Every element's and attribute's exact source span, element
335    /// order, attribute spelling (KML geometry `coordinates`, GPX `lat`/`lon`), and
336    /// the namespace declaration are preserved; entity references are surfaced
337    /// literally (never expanded). It is computed on demand from the exact source
338    /// (its single dependency is the `DocumentExact` root, keyed by `sha256(source)`
339    /// per ADR-0060); a GIS document has no package layer. Derived, never exact.
340    GisModel = 0x38,
341    /// The canonical, representation-preserving Jupyter notebook (`.ipynb`, nbformat)
342    /// model (Phase 21.24): the whole source parsed by the **shared JSON parser** into a
343    /// span-preserving JSON arena (exact token spans, member order, duplicate keys,
344    /// numeric/escape spelling) plus the notebook semantic anchors — the exact
345    /// `nbformat`/`nbformat_minor` tokens, the root `metadata` object node, and the
346    /// ordered cell anchors. Every cell preserves its exact `cell_type` string
347    /// (`code`/`markdown`/`raw`, or any other), its `source` **exactly as written** (a
348    /// single string or an array of line strings, never re-joined or normalized), its
349    /// `execution_count`/`metadata`/`attachments` when present, and its ordered
350    /// `outputs` with each output's exact `output_type` string
351    /// (`stream`/`execute_result`/`display_data`/`error`, or any other) and every field
352    /// token, including a `stream` `text` and an `execute_result`/`display_data` `data`
353    /// representation. It is computed on demand from the exact source (its single
354    /// dependency is the `DocumentExact` root, keyed by `sha256(source)` per ADR-0060);
355    /// a notebook has no package layer. Derived, never exact.
356    NotebookModel = 0x39,
357    /// The canonical, representation-preserving fixed-width (column-position) model
358    /// (Phase 21.25): the whole source parsed into a bounded table whose columns are
359    /// defined by **character positions** (not a delimiter). It records the inferred
360    /// layout (per-column start/end positions and widths, the uniform record width, the
361    /// line terminator, a BOM, and the mixed-terminator fact), the optional header row
362    /// (record 0), and every record's exact content span and per-column field spans
363    /// (padding preserved). It is computed on demand from the exact source (its single
364    /// dependency is the `DocumentExact` root, keyed by `sha256(source)` per ADR-0060);
365    /// a fixed-width document has no package layer. Derived, never exact.
366    FixedWidthModel = 0x3A,
367    /// The canonical, representation-preserving reStructuredText (Docutils) model
368    /// (Phase 21.26.1): the whole source parsed into a bounded, span-preserving prose
369    /// model — section titles with their exact underline/overline adornment (char and
370    /// length) and a recorded hierarchy, paragraphs, explicit markup (`.. ` comments,
371    /// `.. directive::` directives preserved verbatim, substitution definitions,
372    /// footnotes/citations, hyperlink targets), field/option/definition lists, literal
373    /// (`::`) and doctest (`>>> `) blocks, bullet/enumerated lists with nesting, inline
374    /// spans (strong/emphasis/literal/interpreted/substitution/footnote/hyperlink/
375    /// anonymous references), and grid/simple tables. Every span is exact and the
376    /// source is never re-flowed or normalized. It is computed on demand from the exact
377    /// source (its single dependency is the `DocumentExact` root, keyed by
378    /// `sha256(source)` per ADR-0060); a reST document has no package layer. Derived,
379    /// never exact.
380    RstModel = 0x3B,
381    /// The canonical, representation-preserving AsciiDoc model (Phase 21.26.2): the
382    /// whole source parsed into a bounded, span-preserving prose model — a level-0
383    /// document title and `==`+ sections with their exact `=` marker and recorded
384    /// level, paragraphs, document attributes (`:name: value` / `:name!:`) with
385    /// attribute references (`{name}`) surfaced literally (never expanded), block
386    /// attribute lines attached to the following block, delimited blocks (listing /
387    /// literal / example / sidebar / quote / open / passthrough) with their exact
388    /// delimiter and verbatim content, unordered/ordered/description lists with
389    /// nesting, tables (`|===`), admonitions (`NOTE:` / `[NOTE]`), and inline spans
390    /// (strong/emphasis/mono/passthrough/superscript/subscript/mark plus the `link:`,
391    /// `image:`, `include::`, `xref:` and bare-URL macros, preserved verbatim). Every
392    /// span is exact and the source is never re-flowed or normalized. It is computed
393    /// on demand from the exact source (its single dependency is the `DocumentExact`
394    /// root, keyed by `sha256(source)` per ADR-0060); an AsciiDoc document has no
395    /// package layer. Derived, never exact.
396    AsciidocModel = 0x3C,
397    /// The canonical, representation-preserving MDX (Markdown + JSX/ESM) model
398    /// (Phase 21.26.3): the reused Markdown prose model of the whole source **plus** the
399    /// MDX-specific arenas — top-level ESM `import`/`export` statements, JSX elements
400    /// and fragments with their attributes and nested children (recorded as extents,
401    /// never executed and never parsed as JavaScript), and MDX `{ … }` expressions
402    /// inline and block (brace-balanced with string literals, escapes, and comments
403    /// respected) — and the bit-set of MDX signals observed. Every span is exact and
404    /// the source is never re-flowed or normalized. It is computed on demand from the
405    /// exact source (its single dependency is the `DocumentExact` root, keyed by
406    /// `sha256(source)` per ADR-0060); an MDX document has no package layer. Derived,
407    /// never exact.
408    MdxModel = 0x3D,
409    /// The canonical, representation-preserving MHTML (MIME HTML, RFC 2557) web
410    /// archive model (Phase 21.27): the whole source parsed (via the **reused**
411    /// bounded EML MIME layer) into a span-preserving MIME envelope and ordered parts,
412    /// plus the MHTML-specific anchors — the top-level `Snapshot-Content-Location`/
413    /// `Content-Base`, the ordered sub-resources keyed by `Content-Location`/
414    /// `Content-ID`, and the root HTML part (the `start=` `Content-ID`, else the first
415    /// `text/html` part, else the first part). The root part's **decoded** body is
416    /// parsed by the **reused** bounded HTML scanner into a span-preserving
417    /// [`HtmlModel`](crate::adapter::html::HtmlModel) whose spans are relative to that
418    /// decoded body. It is computed on demand from the exact source (its single
419    /// dependency is the `DocumentExact` root, keyed by `sha256(source)` per
420    /// ADR-0060); MHTML has no package layer. Derived, never exact.
421    MhtmlModel = 0x3E,
422    /// The canonical, representation-preserving syslog / log-stream model (Phase
423    /// 21.28): the whole source split into physical lines, each non-blank line
424    /// classified by a recorded dialect (RFC 5424 syslog, RFC 3164 (BSD) syslog, or a
425    /// generic application log line) into a bounded record with its **exact line
426    /// span** and terminator, its physical line number, its decoded priority (when a
427    /// syslog `<PRI>` is present), the deepest structured-data nesting, and its
428    /// **exact-span fields** (PRI/version/timestamp/hostname/app-name/procid/msgid/
429    /// structured-data element/id/parameter-name/parameter-value/tag/pid/level/msg).
430    /// Messages, NILVALUE (`-`), and timestamp/level spelling are preserved verbatim
431    /// and never normalized. It is computed on demand from the exact source (its
432    /// single dependency is the `DocumentExact` root, keyed by `sha256(source)` per
433    /// ADR-0060); a log stream has no package layer. Derived, never exact.
434    LogstreamModel = 0x3F,
435    /// The canonical, representation-preserving package-metadata model (Phase 21.29):
436    /// the reused JSON/TOML arena (member order, duplicate keys, numeric and
437    /// string-escape spelling, and exact token spans all preserved) **plus** a bounded
438    /// semantic projection — every recorded **section** (a named table /
439    /// array-of-tables / array element in TOML, or an object / array member in JSON)
440    /// with its exact name and value spans, and every recorded key/value **entry** with
441    /// its exact key and value spans, grouped by section in document order, and the
442    /// **recorded dialect** (`npm_package` / `cargo_manifest` / `pyproject_manifest` /
443    /// `cargo_lock` / `npm_lock`). Nothing is normalized or re-serialized. It is
444    /// computed on demand from the exact source (its single dependency is the
445    /// `DocumentExact` root, keyed by `sha256(source)` per ADR-0060); a manifest has no
446    /// package layer. Derived, never exact.
447    PkgmetaModel = 0x40,
448    /// The canonical, representation-preserving API/specification model
449    /// (Phase 21.30): the whole source parsed by the **shared JSON parser** into a
450    /// bounded, representation-preserving JSON arena (exact token spans, member order,
451    /// duplicate keys, numeric/escape spelling) plus the API-spec semantic projection:
452    /// the **recorded dialect** (`json_schema` / `openapi` / `swagger` / `asyncapi`),
453    /// the exact spec-version token, every recorded **object** (with its role, exact
454    /// value span, naming span, and member range), every **member** (exact key/value
455    /// spans), and every **`$ref`** member (target preserved verbatim, never resolved).
456    /// Nothing is normalized, resolved, or re-serialized. It is computed on demand from
457    /// the exact source (its single dependency is the `DocumentExact` root, keyed by
458    /// `sha256(source)` per ADR-0060); an API spec has no package layer. Derived, never
459    /// exact.
460    ApispecModel = 0x41,
461    /// The canonical, representation-preserving bibliography model (Phase 21.31): one
462    /// adapter covering **BibTeX**, **RIS**, and **CSL-JSON**, with the **recorded
463    /// dialect** (`bibtex` / `ris` / `csl_json`). For the text dialects it is the
464    /// entry/field arena (entry key, entry type and field names *as written*, field
465    /// order, every value's exact spelling and delimiters, duplicate-field flags, RIS
466    /// tag separator and multi-line continuation spans); for CSL-JSON it embeds the
467    /// reused JSON arena (member order, duplicate keys, numeric/string-escape spelling,
468    /// exact token spans) plus the item projection (each item's string `id` and `type`
469    /// node). Nothing is normalized or re-serialized. It is computed on demand from the
470    /// exact source (its single dependency is the `DocumentExact` root, keyed by
471    /// `sha256(source)` per ADR-0060); a bibliography has no package layer. Derived,
472    /// never exact.
473    BibModel = 0x42,
474    /// The canonical, representation-preserving RTF (Rich Text Format) model
475    /// (Phase 21.32): the whole source parsed into a bounded, span-preserving
476    /// group-brace model — the header (`\rtfN` version, charset, `\deffN`), every
477    /// group with its exact brace span and nesting depth, every control word (with its
478    /// optional numeric parameter) and control symbol, every `\'hh` hex escape and
479    /// `\uN` Unicode escape (with the active `\ucN` fallback count), every destination
480    /// (`\fonttbl`/`\colortbl`/`\stylesheet`/`\info`/`\pict`/`\object`/`\*`) verbatim,
481    /// and every plain text run byte-exactly (never whitespace-normalized). Nothing is
482    /// normalized or re-serialized. It is computed on demand from the exact source (its
483    /// single dependency is the `DocumentExact` root, keyed by `sha256(source)` per
484    /// ADR-0060); RTF has no package layer. Derived, never exact.
485    RtfModel = 0x43,
486    /// The canonical, representation-preserving generic archive-container model
487    /// (Phase 21.33): one adapter covering **ZIP** and **TAR** with the **recorded
488    /// dialect** (`zip`/`tar`). For ZIP it reuses the byte-authoritative physical
489    /// layer ([`crate::adapter::package::zip`]) and records the ordered
490    /// central-directory member list — raw name bytes (case, UTF-8 flag bit 11, and
491    /// non-UTF-8 bytes preserved), compression method, both declared sizes, CRC-32,
492    /// local-header offset, and each member's exact raw byte span — plus the
493    /// end-of-central-directory record and archive comment. For TAR it records the
494    /// standard 512-byte headers (name/prefix, mode, uid/gid, size, mtime, typeflag,
495    /// linkname, `ustar` magic/version), the old-GNU/v7 variant, the GNU long-name
496    /// (`L`)/long-link (`K`) extensions and the PAX (`x`/`g`) records folded into the
497    /// following member, and every member's exact (512-byte-aligned) span. Nothing is
498    /// unzipped, re-zipped, decompressed, or re-serialized. It is computed on demand
499    /// from the exact source (its single dependency is the `DocumentExact` or
500    /// `PackageRoot` node, keyed by `sha256(source)` per ADR-0060); an archive has no
501    /// separate package layer in this adapter. Derived, never exact.
502    ContainerModel = 0x44,
503    /// The canonical, representation-preserving scientific-container model (Phase
504    /// 21.34): one adapter covering **NetCDF classic** (CDF-1/CDF-2) and a documented
505    /// **bounded subset of HDF5**, with the **recorded dialect** (`netcdf`/`hdf5`). For
506    /// NetCDF it records the magic/`numrecs`, the dimension table (with the unlimited
507    /// dimension), the global attribute list, and the variable list (name, dimensions,
508    /// attributes, type, `vsize`, `begin`) plus the record/array data layout. For HDF5
509    /// it records the superblock, the group/dataset tree reached through the symbol-
510    /// table link traversal, each dataset's datatype / simple-or-scalar dataspace /
511    /// contiguous data layout, and the v1 object-header attribute messages. Everything
512    /// outside the HDF5 subset declines typed. Nothing is decompressed, re-serialized,
513    /// or rewritten. It is computed on demand from the exact source (its single
514    /// dependency is the `DocumentExact` root, keyed by `sha256(source)` per ADR-0060);
515    /// neither dialect has a separate package layer in this adapter. Derived, never
516    /// exact.
517    HdfModel = 0x45,
518    /// The canonical, representation-preserving CFBF / OLE2 compound-file model (Phase
519    /// 21.35): the physical layer behind legacy Word/Excel/PowerPoint/Outlook files,
520    /// with the **recorded dialect** (`cfb3`/`cfb4`). It records the 512-byte header
521    /// (signature, version, byte order, sector/mini-sector shifts, counts, first
522    /// sectors), the directory entries (verbatim UTF-16LE names, object type, color,
523    /// the left/right/child sibling tree, start sector, stream size) with the
524    /// red-black sibling/child tree reconstructed into a `parent`/`depth`, and every
525    /// stream resolved to an exact byte span (regular streams through the FAT, small
526    /// streams through the MiniFAT inside the root entry's mini stream). Nothing is
527    /// un-OLE'd or re-OLE'd. It is computed on demand from the exact source (its single
528    /// dependency is the `DocumentExact` root, keyed by `sha256(source)` per ADR-0060);
529    /// a compound file has no separate package layer in this adapter. Derived, never
530    /// exact.
531    CfbfModel = 0x46,
532    /// The canonical, representation-preserving legacy Word 97–2003 (`.doc`, MS-DOC)
533    /// **semantic** model (Phase 26.1), built on the CFBF/OLE2 physical layer (21.35).
534    /// It **reuses the CFBF resolver** (never a second OLE parser) to obtain the
535    /// `WordDocument` stream and the table stream (`0Table`/`1Table` per the FIB
536    /// `fWhichTblStm` flag), records the Word FIB — `wIdent` `0xA5EC`, the `nFib` with
537    /// its recorded Word version, the `fWhichTblStm` flag, `fcMin`/`fcMac`, the
538    /// `ccpText`/`ccpFtn`/`ccpHdd`/… section counts, and `fcClx`/`lcbClx` — and parses
539    /// the **piece table (CLX)**: the zero-or-more `Prc` (0x01) records, the `Pcdt`
540    /// (0x02) record, and its `PlcPcd` (`n+1` CP values plus `n` 8-byte PCDs). Every
541    /// piece keeps its resolved `fc`, its **compressed** flag (a compressed piece is
542    /// CP1252 single-byte text, an uncompressed piece UTF-16LE), its **exact byte span
543    /// in the `WordDocument` stream**, and its decoded text. Nothing is re-encoded or
544    /// re-flowed. It is computed on demand from the exact source (its single dependency
545    /// is the `DocumentExact` root, keyed by `sha256(source)` per ADR-0060); a `.doc`
546    /// has no separate package layer in this adapter. Derived, never exact.
547    DocModel = 0x47,
548    /// The canonical, representation-preserving legacy Excel 97–2003 (`.xls`, BIFF8)
549    /// **semantic** model (Phase 26.2), built on the CFBF/OLE2 physical layer (21.35).
550    /// It **reuses the CFBF resolver** (never a second OLE parser) to obtain the
551    /// `Workbook` stream (the legacy `Book` name is accepted too), records the BIFF8
552    /// record stream — every `[u16 id][u16 length][payload]` record with its exact byte
553    /// span and stream offset, the `BOF`/`EOF` substream framing, and the record's
554    /// fields — the `BOUNDSHEET` sheets (name, `BOF` stream position, visibility, and
555    /// type, in order), the `SST` shared-string table (with `CONTINUE` reassembly of
556    /// continued strings), and the per-sheet cell records
557    /// (`LABELSST`/`NUMBER`/`RK`/`BOOLERR`/`FORMULA`+`STRING`/`BLANK`/`MULBLANK`) with
558    /// their row/column, exact record span, and exact stored value spelling (`RK`'s
559    /// compact numeric encoding is decoded). `DIMENSIONS`/`ROW`/`FORMAT`/`XF` are
560    /// recorded by span. Nothing is normalized or re-serialized. It is computed on
561    /// demand from the exact source (its single dependency is the `DocumentExact` root,
562    /// keyed by `sha256(source)` per ADR-0060); a `.xls` has no separate package layer
563    /// in this adapter. Derived, never exact.
564    XlsModel = 0x48,
565    /// The canonical, representation-preserving legacy PowerPoint 97–2003 (`.ppt`,
566    /// MS-PPT) **semantic** model (Phase 26.3), built on the CFBF/OLE2 physical layer
567    /// (21.35). It **reuses the CFBF resolver** (never a second OLE parser) to obtain
568    /// the `PowerPoint Document` stream as an exact byte span, records the MS-PPT
569    /// record stream — every 8-byte `RecordHeader` (`recVer`/`recInstance`/`recType`/
570    /// `recLen`) with its exact span, stream offset, and nesting depth — the `Document`
571    /// container, the `SlideListWithText` containers' `SlidePersistAtom` entries
572    /// (slide order by `persistIdRef`), and the `TextCharsAtom` (UTF-16LE) /
573    /// `TextBytesAtom` (CP1252) runs with their exact spans and decoded text; the
574    /// `Current User` stream's `UserEditAtom` offset is recorded when it resolves.
575    /// Nothing is normalized or re-serialized. It is computed on demand from the exact
576    /// source (its single dependency is the `DocumentExact` root, keyed by
577    /// `sha256(source)` per ADR-0060); a `.ppt` has no separate package layer in this
578    /// adapter. Derived, never exact.
579    PptModel = 0x49,
580    /// The canonical, representation-preserving legacy Outlook message (`.msg`,
581    /// MS-OXMSG) **semantic** model (Phase 26.4), built on the CFBF/OLE2 physical layer
582    /// (21.35). It **reuses the CFBF resolver** (never a second OLE parser) to obtain
583    /// the root `__properties_version1.0` stream, the root `__substg1.0_<PPPPTTTT>`
584    /// variable-size streams, the optional `__nameid_version1.0` stream, and the
585    /// recipient (`__recip_version1.0_<id>`) / attachment (`__attach_version1.0_<id>`)
586    /// sub-storages as exact byte spans. It records the message header (`Reserved`,
587    /// `NextRecipientId`, `NextAttachmentId`, `RecipientCount`, `AttachmentCount`, and
588    /// the trailing `Reserved`), the fixed-size 16-byte property entries (raw tag, flags,
589    /// value bytes, exact span, owner) in order, the variable-size substreams with type
590    /// `0x001F` (UTF-16LE) / `0x001E` (ANSI/CP1252) decoded and `0x0102` (binary) kept
591    /// as exact bytes, and every recipient/attachment sub-storage's own property list
592    /// and substreams (order and ids preserved). Nothing is normalized or re-serialized.
593    /// It is computed on demand from the exact source (its single dependency is the
594    /// `DocumentExact` root, keyed by `sha256(source)` per ADR-0060); a `.msg` has no
595    /// separate package layer in this adapter. Derived, never exact.
596    MsgModel = 0x4A,
597}
598
599impl NodeKind {
600    /// Map a raw kind byte.
601    pub const fn from_u8(b: u8) -> Option<NodeKind> {
602        Some(match b {
603            0x01 => NodeKind::DocumentExact,
604            0x02 => NodeKind::SourceSlice,
605            0x03 => NodeKind::PdfRevision,
606            0x04 => NodeKind::PdfObject,
607            0x05 => NodeKind::PdfStreamEncoded,
608            0x06 => NodeKind::PdfStreamDecoded,
609            0x07 => NodeKind::ContentOperators,
610            0x08 => NodeKind::TextRuns,
611            0x09 => NodeKind::PageContent,
612            0x0A => NodeKind::PagePreview,
613            0x0B => NodeKind::ResourceRef,
614            0x0C => NodeKind::Concat,
615            0x0D => NodeKind::Literal,
616            0x0E => NodeKind::PackageRoot,
617            0x0F => NodeKind::PackageMemberRaw,
618            0x10 => NodeKind::PackageMemberDecoded,
619            0x11 => NodeKind::PackageOpcModel,
620            0x12 => NodeKind::DocxModel,
621            0x13 => NodeKind::DocxStory,
622            0x14 => NodeKind::EpubModel,
623            0x15 => NodeKind::EpubContent,
624            0x16 => NodeKind::ResourceBlob,
625            0x17 => NodeKind::OdtModel,
626            0x18 => NodeKind::OdtContent,
627            0x19 => NodeKind::PdfRevisionLineage,
628            0x1A => NodeKind::XlsxModel,
629            0x1B => NodeKind::XlsxWorkbook,
630            0x1C => NodeKind::XlsxSheet,
631            0x1D => NodeKind::PptxModel,
632            0x1E => NodeKind::PptxPresentation,
633            0x1F => NodeKind::PptxSlide,
634            0x20 => NodeKind::PptxNotes,
635            0x21 => NodeKind::OdsModel,
636            0x22 => NodeKind::OdsContent,
637            0x23 => NodeKind::OdsStyles,
638            0x24 => NodeKind::OdpModel,
639            0x25 => NodeKind::OdpContent,
640            0x26 => NodeKind::OdpStyles,
641            0x27 => NodeKind::JsonModel,
642            0x28 => NodeKind::YamlModel,
643            0x29 => NodeKind::CsvModel,
644            0x2A => NodeKind::MarkdownModel,
645            0x2B => NodeKind::XmlModel,
646            0x2C => NodeKind::HtmlModel,
647            0x2D => NodeKind::TomlModel,
648            0x2E => NodeKind::JsonlModel,
649            0x2F => NodeKind::EmlModel,
650            0x30 => NodeKind::ParquetModel,
651            0x31 => NodeKind::ArrowModel,
652            0x32 => NodeKind::Json5Model,
653            0x33 => NodeKind::CborModel,
654            0x34 => NodeKind::MsgpackModel,
655            0x35 => NodeKind::ConfigModel,
656            0x36 => NodeKind::FeedModel,
657            0x37 => NodeKind::GeojsonModel,
658            0x38 => NodeKind::GisModel,
659            0x39 => NodeKind::NotebookModel,
660            0x3A => NodeKind::FixedWidthModel,
661            0x3B => NodeKind::RstModel,
662            0x3C => NodeKind::AsciidocModel,
663            0x3D => NodeKind::MdxModel,
664            0x3E => NodeKind::MhtmlModel,
665            0x3F => NodeKind::LogstreamModel,
666            0x40 => NodeKind::PkgmetaModel,
667            0x41 => NodeKind::ApispecModel,
668            0x42 => NodeKind::BibModel,
669            0x43 => NodeKind::RtfModel,
670            0x44 => NodeKind::ContainerModel,
671            0x45 => NodeKind::HdfModel,
672            0x46 => NodeKind::CfbfModel,
673            0x47 => NodeKind::DocModel,
674            0x48 => NodeKind::XlsModel,
675            0x49 => NodeKind::PptModel,
676            0x4A => NodeKind::MsgModel,
677            _ => return None,
678        })
679    }
680
681    /// Stable short name.
682    pub const fn name(self) -> &'static str {
683        match self {
684            NodeKind::DocumentExact => "DocumentExact",
685            NodeKind::SourceSlice => "SourceSlice",
686            NodeKind::PdfRevision => "PdfRevision",
687            NodeKind::PdfObject => "PdfObject",
688            NodeKind::PdfStreamEncoded => "PdfStreamEncoded",
689            NodeKind::PdfStreamDecoded => "PdfStreamDecoded",
690            NodeKind::ContentOperators => "ContentOperators",
691            NodeKind::TextRuns => "TextRuns",
692            NodeKind::PageContent => "PageContent",
693            NodeKind::PagePreview => "PagePreview",
694            NodeKind::ResourceRef => "ResourceRef",
695            NodeKind::Concat => "Concat",
696            NodeKind::Literal => "Literal",
697            NodeKind::PackageRoot => "PackageRoot",
698            NodeKind::PackageMemberRaw => "PackageMemberRaw",
699            NodeKind::PackageMemberDecoded => "PackageMemberDecoded",
700            NodeKind::PackageOpcModel => "PackageOpcModel",
701            NodeKind::DocxModel => "DocxModel",
702            NodeKind::DocxStory => "DocxStory",
703            NodeKind::EpubModel => "EpubModel",
704            NodeKind::EpubContent => "EpubContent",
705            NodeKind::ResourceBlob => "ResourceBlob",
706            NodeKind::OdtModel => "OdtModel",
707            NodeKind::OdtContent => "OdtContent",
708            NodeKind::PdfRevisionLineage => "PdfRevisionLineage",
709            NodeKind::XlsxModel => "XlsxModel",
710            NodeKind::XlsxWorkbook => "XlsxWorkbook",
711            NodeKind::XlsxSheet => "XlsxSheet",
712            NodeKind::PptxModel => "PptxModel",
713            NodeKind::PptxPresentation => "PptxPresentation",
714            NodeKind::PptxSlide => "PptxSlide",
715            NodeKind::PptxNotes => "PptxNotes",
716            NodeKind::OdsModel => "OdsModel",
717            NodeKind::OdsContent => "OdsContent",
718            NodeKind::OdsStyles => "OdsStyles",
719            NodeKind::OdpModel => "OdpModel",
720            NodeKind::OdpContent => "OdpContent",
721            NodeKind::OdpStyles => "OdpStyles",
722            NodeKind::JsonModel => "JsonModel",
723            NodeKind::YamlModel => "YamlModel",
724            NodeKind::CsvModel => "CsvModel",
725            NodeKind::MarkdownModel => "MarkdownModel",
726            NodeKind::XmlModel => "XmlModel",
727            NodeKind::HtmlModel => "HtmlModel",
728            NodeKind::TomlModel => "TomlModel",
729            NodeKind::JsonlModel => "JsonlModel",
730            NodeKind::EmlModel => "EmlModel",
731            NodeKind::ParquetModel => "ParquetModel",
732            NodeKind::ArrowModel => "ArrowModel",
733            NodeKind::Json5Model => "Json5Model",
734            NodeKind::CborModel => "CborModel",
735            NodeKind::MsgpackModel => "MsgpackModel",
736            NodeKind::ConfigModel => "ConfigModel",
737            NodeKind::FeedModel => "FeedModel",
738            NodeKind::GeojsonModel => "GeojsonModel",
739            NodeKind::GisModel => "GisModel",
740            NodeKind::NotebookModel => "NotebookModel",
741            NodeKind::FixedWidthModel => "FixedWidthModel",
742            NodeKind::RstModel => "RstModel",
743            NodeKind::AsciidocModel => "AsciidocModel",
744            NodeKind::MdxModel => "MdxModel",
745            NodeKind::MhtmlModel => "MhtmlModel",
746            NodeKind::LogstreamModel => "LogstreamModel",
747            NodeKind::PkgmetaModel => "PkgmetaModel",
748            NodeKind::ApispecModel => "ApispecModel",
749            NodeKind::BibModel => "BibModel",
750            NodeKind::RtfModel => "RtfModel",
751            NodeKind::ContainerModel => "ContainerModel",
752            NodeKind::HdfModel => "HdfModel",
753            NodeKind::CfbfModel => "CfbfModel",
754            NodeKind::DocModel => "DocModel",
755            NodeKind::XlsModel => "XlsModel",
756            NodeKind::PptModel => "PptModel",
757            NodeKind::MsgModel => "MsgModel",
758        }
759    }
760
761    /// Whether the output bytes are an exact, byte-identical observation of the
762    /// source (`true`) or a deterministic derived projection (`false`).
763    ///
764    /// This is the `Q_ref` / `Q_gen` boundary (ADR-0026).
765    pub const fn is_exact(self) -> bool {
766        matches!(
767            self,
768            NodeKind::DocumentExact
769                | NodeKind::SourceSlice
770                | NodeKind::PdfRevision
771                | NodeKind::PdfObject
772                | NodeKind::PdfStreamEncoded
773                | NodeKind::ResourceRef
774                | NodeKind::Concat
775                | NodeKind::Literal
776                // Package physical leaves are exact source spans; a decoded
777                // member (`PackageMemberDecoded`) is derived and is **not** exact.
778                | NodeKind::PackageRoot
779                | NodeKind::PackageMemberRaw
780                // A shared resource is the exact embedded bytes.
781                | NodeKind::ResourceBlob
782        )
783    }
784}
785
786/// A bounded resource envelope declared by a node.
787#[derive(Debug, Clone, Copy, PartialEq, Eq)]
788pub struct NodeLimits {
789    /// Maximum materialized output length this node may produce.
790    pub max_output_bytes: u64,
791    /// Maximum dependency depth permitted below this node.
792    pub max_depth: u16,
793    /// Maximum dependency fanout permitted.
794    pub max_fanout: u16,
795}
796
797impl NodeLimits {
798    /// Conservative defaults used by the PDF inverse compiler.
799    pub const DEFAULT: NodeLimits = NodeLimits {
800        max_output_bytes: 1 << 31,
801        max_depth: 64,
802        max_fanout: MAX_NODE_DEPS as u16,
803    };
804}
805
806/// One canonical procedural seed node.
807#[derive(Debug, Clone, PartialEq, Eq)]
808pub struct SeedNode {
809    /// Computation kind.
810    pub kind: NodeKind,
811    /// Materializer registry id (the kind value for v1).
812    pub materializer_id: u16,
813    /// Materializer semantics version.
814    pub materializer_version: u16,
815    /// Declared logical output length (a bound, checked on materialization).
816    pub logical_output_len: u64,
817    /// Kind-specific canonical parameters.
818    pub params: Vec<u8>,
819    /// Canonical dependency ids actually read (the dynamic read set).
820    pub deps: Vec<NodeId>,
821    /// Short provenance/basis string (adapter-supplied, advisory).
822    pub provenance: String,
823    /// Resource envelope.
824    pub limits: NodeLimits,
825}
826
827impl SeedNode {
828    /// Construct a node with the current materializer version.
829    pub fn new(
830        kind: NodeKind,
831        logical_output_len: u64,
832        params: Vec<u8>,
833        deps: Vec<NodeId>,
834        provenance: impl Into<String>,
835    ) -> Self {
836        SeedNode {
837            kind,
838            materializer_id: kind as u16,
839            materializer_version: MATERIALIZER_VERSION,
840            logical_output_len,
841            params,
842            deps,
843            provenance: provenance.into(),
844            limits: NodeLimits::DEFAULT,
845        }
846    }
847
848    /// Canonically encode this node. The encoding never includes the node id.
849    pub fn encode_canonical(&self) -> Vec<u8> {
850        let mut out = Vec::with_capacity(64 + self.params.len());
851        out.push(NODE_MAGIC);
852        out.push(crate::store::SEED_FORMAT_VERSION);
853        out.push(self.kind as u8);
854        out.push(0); // reserved
855        out.extend_from_slice(&self.materializer_id.to_le_bytes());
856        out.extend_from_slice(&self.materializer_version.to_le_bytes());
857        out.extend_from_slice(&self.logical_output_len.to_le_bytes());
858        out.extend_from_slice(&(self.deps.len() as u32).to_le_bytes());
859        out.extend_from_slice(&(self.params.len() as u32).to_le_bytes());
860        out.extend_from_slice(&self.limits.max_output_bytes.to_le_bytes());
861        out.extend_from_slice(&self.limits.max_depth.to_le_bytes());
862        out.extend_from_slice(&self.limits.max_fanout.to_le_bytes());
863        for dep in &self.deps {
864            out.extend_from_slice(dep.as_bytes());
865        }
866        out.extend_from_slice(&self.params);
867        let prov = self.provenance.as_bytes();
868        out.extend_from_slice(&(prov.len() as u32).to_le_bytes());
869        out.extend_from_slice(prov);
870        out
871    }
872
873    /// Parse a canonical node, enforcing structural bounds.
874    pub fn decode_canonical(bytes: &[u8]) -> Result<SeedNode> {
875        let mut r = Reader::new(bytes);
876        if r.u8()? != NODE_MAGIC {
877            return Err(Error::unsupported_version("seed node: bad magic"));
878        }
879        let version = r.u8()?;
880        if version != crate::store::SEED_FORMAT_VERSION {
881            return Err(Error::unsupported_version(format!(
882                "seed node format version {version} is not supported"
883            )));
884        }
885        let kind_byte = r.u8()?;
886        let kind = NodeKind::from_u8(kind_byte)
887            .ok_or_else(|| Error::unsupported_version(format!("unknown node kind {kind_byte}")))?;
888        let _reserved = r.u8()?;
889        let materializer_id = r.u16()?;
890        let materializer_version = r.u16()?;
891        let logical_output_len = r.u64()?;
892        let dep_count = r.u32()? as usize;
893        let param_len = r.u32()? as usize;
894        let max_output_bytes = r.u64()?;
895        let max_depth = r.u16()?;
896        let max_fanout = r.u16()?;
897        if dep_count > MAX_NODE_DEPS {
898            return Err(Error::resource_limit(format!(
899                "seed node declares {dep_count} deps (max {MAX_NODE_DEPS})"
900            )));
901        }
902        let mut deps = Vec::with_capacity(dep_count);
903        for _ in 0..dep_count {
904            let mut b = [0u8; 32];
905            b.copy_from_slice(r.bytes(32)?);
906            deps.push(NodeId::from_bytes(b));
907        }
908        let params = r.bytes(param_len)?.to_vec();
909        let prov_len = r.u32()? as usize;
910        let prov = r.bytes(prov_len)?;
911        let provenance = core::str::from_utf8(prov)
912            .map_err(|_| Error::usage("seed node provenance is not UTF-8"))?
913            .to_string();
914        if !r.at_end() {
915            return Err(Error::usage("seed node has trailing bytes"));
916        }
917        if materializer_id != kind as u16 {
918            return Err(Error::unsupported_version(
919                "seed node materializer id does not match its kind",
920            ));
921        }
922        Ok(SeedNode {
923            kind,
924            materializer_id,
925            materializer_version,
926            logical_output_len,
927            params,
928            deps,
929            provenance,
930            limits: NodeLimits {
931                max_output_bytes,
932                max_depth,
933                max_fanout,
934            },
935        })
936    }
937
938    /// The content id of this node's canonical encoding.
939    pub fn content_id(&self) -> NodeId {
940        NodeId::of_node(&self.encode_canonical())
941    }
942
943    /// Validate the declared envelope against the global [`Limits`].
944    pub fn check_limits(&self, limits: &Limits) -> Result<()> {
945        if self.logical_output_len > self.limits.max_output_bytes {
946            return Err(Error::resource_limit(format!(
947                "seed node declares output {} > its own cap {}",
948                self.logical_output_len, self.limits.max_output_bytes
949            )));
950        }
951        let _ = limits;
952        Ok(())
953    }
954}
955
956/// A tiny bounds-checked little-endian reader.
957struct Reader<'a> {
958    b: &'a [u8],
959    at: usize,
960}
961
962impl<'a> Reader<'a> {
963    fn new(b: &'a [u8]) -> Self {
964        Reader { b, at: 0 }
965    }
966    fn bytes(&mut self, n: usize) -> Result<&'a [u8]> {
967        let end = self
968            .at
969            .checked_add(n)
970            .ok_or_else(|| Error::usage("seed node read overflow"))?;
971        if end > self.b.len() {
972            return Err(Error::usage("truncated seed node"));
973        }
974        let s = &self.b[self.at..end];
975        self.at = end;
976        Ok(s)
977    }
978    fn u8(&mut self) -> Result<u8> {
979        Ok(self.bytes(1)?[0])
980    }
981    fn u16(&mut self) -> Result<u16> {
982        let b = self.bytes(2)?;
983        Ok(u16::from_le_bytes([b[0], b[1]]))
984    }
985    fn u32(&mut self) -> Result<u32> {
986        let b = self.bytes(4)?;
987        Ok(u32::from_le_bytes([b[0], b[1], b[2], b[3]]))
988    }
989    fn u64(&mut self) -> Result<u64> {
990        let b = self.bytes(8)?;
991        let mut a = [0u8; 8];
992        a.copy_from_slice(b);
993        Ok(u64::from_le_bytes(a))
994    }
995    fn at_end(&self) -> bool {
996        self.at == self.b.len()
997    }
998}
999
1000/// Encode an `[offset, len]` parameter block.
1001pub fn span_params(offset: u64, len: u64) -> Vec<u8> {
1002    let mut p = Vec::with_capacity(16);
1003    p.extend_from_slice(&offset.to_le_bytes());
1004    p.extend_from_slice(&len.to_le_bytes());
1005    p
1006}
1007
1008/// Decode an `[offset, len]` parameter block.
1009pub fn read_span_params(params: &[u8]) -> Result<(u64, u64)> {
1010    if params.len() != 16 {
1011        return Err(Error::usage("span params must be 16 bytes"));
1012    }
1013    let mut a = [0u8; 8];
1014    a.copy_from_slice(&params[0..8]);
1015    let offset = u64::from_le_bytes(a);
1016    a.copy_from_slice(&params[8..16]);
1017    let len = u64::from_le_bytes(a);
1018    Ok((offset, len))
1019}
1020
1021/// Encode a `(u32, u16, ...)` object-identity parameter block: `object`,
1022/// `generation`, then a trailing kind-specific `u64`.
1023pub fn object_params(object: u32, generation: u16, extra: u64) -> Vec<u8> {
1024    let mut p = Vec::with_capacity(16);
1025    p.extend_from_slice(&object.to_le_bytes());
1026    p.extend_from_slice(&generation.to_le_bytes());
1027    p.extend_from_slice(&0u16.to_le_bytes());
1028    p.extend_from_slice(&extra.to_le_bytes());
1029    p
1030}
1031
1032/// Decode an object-identity parameter block.
1033pub fn read_object_params(params: &[u8]) -> Result<(u32, u16, u64)> {
1034    if params.len() != 16 {
1035        return Err(Error::usage("object params must be 16 bytes"));
1036    }
1037    let object = u32::from_le_bytes([params[0], params[1], params[2], params[3]]);
1038    let generation = u16::from_le_bytes([params[4], params[5]]);
1039    let mut a = [0u8; 8];
1040    a.copy_from_slice(&params[8..16]);
1041    let extra = u64::from_le_bytes(a);
1042    Ok((object, generation, extra))
1043}
1044
1045/// Encode a single `u32` parameter.
1046pub fn u32_params(v: u32) -> Vec<u8> {
1047    v.to_le_bytes().to_vec()
1048}
1049
1050/// Decode a single `u32` parameter.
1051pub fn read_u32_params(params: &[u8]) -> Result<u32> {
1052    if params.len() != 4 {
1053        return Err(Error::usage("u32 params must be 4 bytes"));
1054    }
1055    Ok(u32::from_le_bytes([
1056        params[0], params[1], params[2], params[3],
1057    ]))
1058}
1059
1060#[cfg(test)]
1061mod tests {
1062    use super::*;
1063
1064    #[test]
1065    fn canonical_roundtrip_is_stable() {
1066        let n = SeedNode::new(
1067            NodeKind::PdfStreamDecoded,
1068            4096,
1069            span_params(100, 200),
1070            vec![NodeId::from_bytes([7u8; 32])],
1071            "pdf:stream-decoded",
1072        );
1073        let enc = n.encode_canonical();
1074        let back = SeedNode::decode_canonical(&enc).unwrap();
1075        assert_eq!(n, back);
1076        assert_eq!(back.content_id(), n.content_id());
1077        assert_eq!(n.encode_canonical(), enc);
1078    }
1079
1080    #[test]
1081    fn dependency_change_changes_id() {
1082        let a = SeedNode::new(
1083            NodeKind::Concat,
1084            10,
1085            vec![],
1086            vec![NodeId::from_bytes([1u8; 32])],
1087            "t",
1088        );
1089        let b = SeedNode::new(
1090            NodeKind::Concat,
1091            10,
1092            vec![],
1093            vec![NodeId::from_bytes([2u8; 32])],
1094            "t",
1095        );
1096        assert_ne!(a.content_id(), b.content_id());
1097    }
1098
1099    #[test]
1100    fn unknown_kind_and_version_fail_closed() {
1101        let mut n = SeedNode::new(NodeKind::Literal, 1, vec![9], vec![], "t").encode_canonical();
1102        n[0] = 0x00;
1103        assert!(SeedNode::decode_canonical(&n).is_err());
1104        let mut n2 = SeedNode::new(NodeKind::Literal, 1, vec![9], vec![], "t").encode_canonical();
1105        n2[1] = 0x7F;
1106        assert!(SeedNode::decode_canonical(&n2).is_err());
1107    }
1108
1109    #[test]
1110    fn trailing_bytes_are_rejected() {
1111        let mut enc = SeedNode::new(NodeKind::Literal, 1, vec![], vec![], "t").encode_canonical();
1112        enc.push(0);
1113        assert!(SeedNode::decode_canonical(&enc).is_err());
1114    }
1115
1116    #[test]
1117    fn param_helpers_roundtrip() {
1118        let (o, l) = read_span_params(&span_params(5, 9)).unwrap();
1119        assert_eq!((o, l), (5, 9));
1120        let (ob, g, e) = read_object_params(&object_params(42, 3, 77)).unwrap();
1121        assert_eq!((ob, g, e), (42, 3, 77));
1122        assert_eq!(read_u32_params(&u32_params(1234)).unwrap(), 1234);
1123    }
1124}