Skip to main content

active_infinity/
ir.rs

1//! Active Infinity's in-memory module model.
2//!
3//! This is the *canonical* IR: the encoder is a pure function of it, and the
4//! decoder reproduces it exactly. It deliberately does **not** mirror the
5//! `active-core` arena layout: the wire format is versioned independently of
6//! any dependency's semver (see ADR-0002). Interop with the text language IR
7//! lives in [`crate::interop`] behind the `active-core` feature.
8//!
9//! Structural invariants upheld by [`InfinityModule::validate_structure`]:
10//!
11//! - every referenced node id exists (no dangling references);
12//! - table fields, binding names and array nodes are unique where required;
13//! - the dependency graph is acyclic (Active rejects cycles before any value
14//!   exists);
15//! - nesting depth stays within the caller's budget.
16//!
17//! Secret material is held in [`SecretBytes`], which has **no** `Display`,
18//! `Debug`, `Serialize` or equality path that can reveal it.
19
20// The variant and field names in this module are the wire contract itself and
21// are documented normatively in `docs/spec/layout.md`; per-variant rustdoc would
22// duplicate that table without adding information.
23#![allow(missing_docs)]
24
25use std::collections::HashMap;
26use std::fmt;
27
28/// Index into the module arena.
29#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, PartialOrd, Ord)]
30pub struct NodeId(pub u32);
31
32impl NodeId {
33    /// Sentinel for "no node".
34    pub const NONE: NodeId = NodeId(u32::MAX);
35
36    pub fn index(self) -> usize {
37        self.0 as usize
38    }
39
40    pub fn is_none(self) -> bool {
41        self == NodeId::NONE
42    }
43}
44
45impl fmt::Display for NodeId {
46    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
47        write!(f, "n{}", self.0)
48    }
49}
50
51/// A secret string held structurally.
52///
53/// The type exposes no way to read the bytes back through a public API: the
54/// wire codec and the `active-core` bridge are the only in-crate readers, and
55/// both are audited. `Debug`, `Display`, `PartialEq`, `Ord` and `Hash` are all
56/// deliberately absent or redacting, so a secret can never leak through
57/// logging, assertion output, sorting or map keys, and can never be compared
58/// for equality (the anti-oracle rule).
59#[derive(Clone)]
60pub struct SecretBytes(Vec<u8>);
61
62impl SecretBytes {
63    /// Wrap raw bytes as a secret.
64    pub fn new(bytes: impl Into<Vec<u8>>) -> Self {
65        SecretBytes(bytes.into())
66    }
67
68    /// Number of bytes — length is not secret material and is used by
69    /// budgets and diagnostics.
70    pub fn len(&self) -> usize {
71        self.0.len()
72    }
73
74    pub fn is_empty(&self) -> bool {
75        self.0.is_empty()
76    }
77
78    /// Read the raw bytes. Crate-internal: only the audited codec and the
79    /// `active-core` bridge call this. There is no public unlock API.
80    pub(crate) fn expose(&self) -> &[u8] {
81        &self.0
82    }
83}
84
85impl fmt::Debug for SecretBytes {
86    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
87        f.write_str("Secret(***)")
88    }
89}
90
91impl fmt::Display for SecretBytes {
92    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
93        f.write_str("***SECRET***")
94    }
95}
96
97impl PartialEq for SecretBytes {
98    /// Anti-oracle: a secret never equals anything, including itself.
99    fn eq(&self, _other: &Self) -> bool {
100        false
101    }
102}
103
104impl From<&str> for SecretBytes {
105    fn from(value: &str) -> Self {
106        SecretBytes(value.as_bytes().to_vec())
107    }
108}
109
110impl From<String> for SecretBytes {
111    fn from(value: String) -> Self {
112        SecretBytes(value.into_bytes())
113    }
114}
115
116/// Binary operators, with permanent wire codes.
117#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
118pub enum BinOp {
119    Add,
120    Sub,
121    Mul,
122    Div,
123    Eq,
124    Ne,
125    Lt,
126    Le,
127    Gt,
128    Ge,
129    And,
130    Or,
131}
132
133impl BinOp {
134    pub const fn code(self) -> u32 {
135        match self {
136            BinOp::Add => 0,
137            BinOp::Sub => 1,
138            BinOp::Mul => 2,
139            BinOp::Div => 3,
140            BinOp::Eq => 4,
141            BinOp::Ne => 5,
142            BinOp::Lt => 6,
143            BinOp::Le => 7,
144            BinOp::Gt => 8,
145            BinOp::Ge => 9,
146            BinOp::And => 10,
147            BinOp::Or => 11,
148        }
149    }
150
151    pub const fn from_code(code: u32) -> Option<Self> {
152        let op = match code {
153            0 => BinOp::Add,
154            1 => BinOp::Sub,
155            2 => BinOp::Mul,
156            3 => BinOp::Div,
157            4 => BinOp::Eq,
158            5 => BinOp::Ne,
159            6 => BinOp::Lt,
160            7 => BinOp::Le,
161            8 => BinOp::Gt,
162            9 => BinOp::Ge,
163            10 => BinOp::And,
164            11 => BinOp::Or,
165            _ => return None,
166        };
167        Some(op)
168    }
169
170    pub const fn as_str(self) -> &'static str {
171        match self {
172            BinOp::Add => "+",
173            BinOp::Sub => "-",
174            BinOp::Mul => "*",
175            BinOp::Div => "/",
176            BinOp::Eq => "==",
177            BinOp::Ne => "!=",
178            BinOp::Lt => "<",
179            BinOp::Le => "<=",
180            BinOp::Gt => ">",
181            BinOp::Ge => ">=",
182            BinOp::And => "and",
183            BinOp::Or => "or",
184        }
185    }
186}
187
188/// Unary operators, with permanent wire codes.
189#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
190pub enum UnOp {
191    Neg,
192    Not,
193}
194
195impl UnOp {
196    pub const fn code(self) -> u32 {
197        match self {
198            UnOp::Neg => 0,
199            UnOp::Not => 1,
200        }
201    }
202
203    pub const fn from_code(code: u32) -> Option<Self> {
204        match code {
205            0 => Some(UnOp::Neg),
206            1 => Some(UnOp::Not),
207            _ => None,
208        }
209    }
210
211    pub const fn as_str(self) -> &'static str {
212        match self {
213            UnOp::Neg => "-",
214            UnOp::Not => "!",
215        }
216    }
217}
218
219/// Semantic tag for a multi-line string block.
220#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
221pub enum SemanticTag {
222    Prompt,
223    Policy,
224    Template,
225}
226
227impl SemanticTag {
228    pub const fn code(self) -> u32 {
229        match self {
230            SemanticTag::Prompt => 0,
231            SemanticTag::Policy => 1,
232            SemanticTag::Template => 2,
233        }
234    }
235
236    pub const fn from_code(code: u32) -> Option<Self> {
237        match code {
238            0 => Some(SemanticTag::Prompt),
239            1 => Some(SemanticTag::Policy),
240            2 => Some(SemanticTag::Template),
241            _ => None,
242        }
243    }
244
245    pub const fn as_str(self) -> &'static str {
246        match self {
247            SemanticTag::Prompt => "prompt",
248            SemanticTag::Policy => "policy",
249            SemanticTag::Template => "template",
250        }
251    }
252}
253
254/// One interpolation part.
255#[derive(Debug, Clone, PartialEq)]
256pub enum InterpPart {
257    Literal(String),
258    Expr(NodeId),
259}
260
261/// A runtime constraint attached to a node (`@constraint` predicates and
262/// schema bounds).
263#[derive(Debug, Clone, PartialEq)]
264pub enum Constraint {
265    MinMax { min: Option<i64>, max: Option<i64> },
266    Enum(Vec<String>),
267    Pattern(String),
268    Odd,
269    Custom(String),
270}
271
272/// Declared value type of a schema field.
273#[derive(Debug, Clone, PartialEq)]
274pub enum TypeSpec {
275    Null,
276    Bool,
277    Int,
278    Float,
279    String,
280    Duration,
281    Bytes,
282    Table { closed: bool },
283    Enum(Vec<String>),
284    Secret,
285    Any,
286    Blob,
287}
288
289impl TypeSpec {
290    pub const fn code(&self) -> u32 {
291        match self {
292            TypeSpec::Null => 0,
293            TypeSpec::Bool => 1,
294            TypeSpec::Int => 2,
295            TypeSpec::Float => 3,
296            TypeSpec::String => 4,
297            TypeSpec::Duration => 5,
298            TypeSpec::Bytes => 6,
299            TypeSpec::Table { .. } => 7,
300            TypeSpec::Enum(_) => 8,
301            TypeSpec::Secret => 9,
302            TypeSpec::Any => 10,
303            TypeSpec::Blob => 11,
304        }
305    }
306
307    /// Human-readable name for diagnostics (mirrors `active-core`).
308    pub fn name(&self) -> String {
309        match self {
310            TypeSpec::Null => "Null".into(),
311            TypeSpec::Bool => "Bool".into(),
312            TypeSpec::Int => "Int".into(),
313            TypeSpec::Float => "Float".into(),
314            TypeSpec::String => "String".into(),
315            TypeSpec::Duration => "Duration".into(),
316            TypeSpec::Bytes => "Bytes".into(),
317            TypeSpec::Table { closed } => {
318                if *closed {
319                    "Table(closed)".into()
320                } else {
321                    "Table".into()
322                }
323            }
324            TypeSpec::Enum(values) => format!("Enum({})", values.join(", ")),
325            TypeSpec::Secret => "Secret".into(),
326            TypeSpec::Any => "Any".into(),
327            TypeSpec::Blob => "Blob".into(),
328        }
329    }
330}
331
332/// A structural patch operation.
333#[derive(Debug, Clone, PartialEq)]
334pub enum PatchOp {
335    Set { path: String, value: NodeId },
336    Remove { path: String },
337    Rename { from: String, to: String },
338}
339
340/// One schema field's constraints.
341#[derive(Debug, Clone, PartialEq)]
342pub struct SchemaFieldSpec {
343    pub typ: TypeSpec,
344    pub min: Option<NodeId>,
345    pub max: Option<NodeId>,
346    pub min_len: Option<NodeId>,
347    pub max_len: Option<NodeId>,
348    pub pattern: Option<NodeId>,
349    pub default: Option<NodeId>,
350    pub deprecated: Option<String>,
351}
352
353impl SchemaFieldSpec {
354    pub fn bare(typ: TypeSpec) -> Self {
355        Self {
356            typ,
357            min: None,
358            max: None,
359            min_len: None,
360            max_len: None,
361            pattern: None,
362            default: None,
363            deprecated: None,
364        }
365    }
366}
367
368/// A module node.
369///
370/// Every variant is a value of the Active IR after name resolution; `Ident`
371/// is retained only because decoded third-party modules may still contain
372/// one (the encoder never emits it for canonical modules produced from
373/// `active-core`).
374///
375/// `Debug` is written by hand (never derived): blob payloads are summarized
376/// by identity and secrets render as `Secret(***)`, so logging a module can
377/// never dump payload or secret bytes.
378#[derive(Clone, PartialEq)]
379pub enum Inode {
380    Null,
381    Bool(bool),
382    Int(i64),
383    Float(f64),
384    Str(String),
385    DurationMs(u64),
386    Quantity {
387        value: NodeId,
388        unit: super::UnitTag,
389    },
390    Array {
391        elements: Vec<NodeId>,
392    },
393    Table {
394        fields: Vec<(String, NodeId)>,
395    },
396    /// A structural secret literal. Never equals anything.
397    Secret(SecretBytes),
398    /// A secret derived from a locator node (`secret.env(NAME)`,
399    /// `secret(<expr>)`).
400    SecretRef {
401        locator: NodeId,
402    },
403    Ref(NodeId),
404    Env {
405        name: String,
406        default: Option<NodeId>,
407    },
408    Ident(String),
409    Interpolate {
410        parts: Vec<InterpPart>,
411    },
412    Binary {
413        op: BinOp,
414        lhs: NodeId,
415        rhs: NodeId,
416    },
417    Unary {
418        op: UnOp,
419        expr: NodeId,
420    },
421    Constrained {
422        target: NodeId,
423        constraint: Constraint,
424    },
425    Patch(PatchOp),
426    SchemaField(SchemaFieldSpec),
427    SchemaTable {
428        fields: Vec<(String, NodeId)>,
429        closed: bool,
430    },
431    SchemaArray {
432        element: NodeId,
433    },
434    Semantic {
435        tag: SemanticTag,
436        inner: NodeId,
437    },
438    /// `blob(media_type, data)` construction site (reserved kind 24).
439    ContentAddressed {
440        media_type: NodeId,
441        data: NodeId,
442    },
443    /// A materialised content-addressed blob identity.
444    ///
445    /// `payload` is present only for untainted blobs that were materialised;
446    /// a tainted blob never carries bytes, and the codec refuses a tainted
447    /// blob entry whose payload is non-empty.
448    Blob {
449        hash: [u8; 32],
450        size: u64,
451        tainted: bool,
452        media_type: String,
453        payload: Option<Vec<u8>>,
454    },
455}
456
457impl fmt::Debug for Inode {
458    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
459        match self {
460            Inode::Null => f.write_str("Null"),
461            Inode::Bool(value) => write!(f, "Bool({value})"),
462            Inode::Int(value) => write!(f, "Int({value})"),
463            Inode::Float(value) => write!(f, "Float({value})"),
464            Inode::Str(value) => write!(f, "Str({value:?})"),
465            Inode::DurationMs(ms) => write!(f, "DurationMs({ms})"),
466            Inode::Quantity { value, unit } => write!(f, "Quantity({value}, {unit})"),
467            Inode::Array { elements } => write!(f, "Array({elements:?})"),
468            Inode::Table { fields } => write!(f, "Table({fields:?})"),
469            Inode::Secret(_) => f.write_str("Secret(***)"),
470            Inode::SecretRef { locator } => write!(f, "SecretRef({locator})"),
471            Inode::Ref(id) => write!(f, "Ref({id})"),
472            Inode::Env { name, default } => write!(f, "Env({name:?}, {default:?})"),
473            Inode::Ident(name) => write!(f, "Ident({name:?})"),
474            Inode::Interpolate { parts } => write!(f, "Interpolate({parts:?})"),
475            Inode::Binary { op, lhs, rhs } => write!(f, "Binary({}, {lhs}, {rhs})", op.as_str()),
476            Inode::Unary { op, expr } => write!(f, "Unary({}, {expr})", op.as_str()),
477            Inode::Constrained { target, constraint } => {
478                write!(f, "Constrained({target}, {constraint:?})")
479            }
480            Inode::Patch(op) => write!(f, "Patch({op:?})"),
481            Inode::SchemaField(spec) => write!(f, "SchemaField({spec:?})"),
482            Inode::SchemaTable { fields, closed } => {
483                write!(f, "SchemaTable({fields:?}, closed={closed})")
484            }
485            Inode::SchemaArray { element } => write!(f, "SchemaArray({element})"),
486            Inode::Semantic { tag, inner } => write!(f, "Semantic({}, {inner})", tag.as_str()),
487            Inode::ContentAddressed { media_type, data } => {
488                write!(f, "ContentAddressed({media_type}, {data})")
489            }
490            Inode::Blob {
491                hash,
492                size,
493                tainted,
494                media_type,
495                payload,
496            } => write!(
497                f,
498                "Blob(blake3:{}, size={size}, media={media_type:?}, tainted={tainted}, payload={})",
499                hash_hex(hash),
500                match payload {
501                    Some(bytes) => format!("{} bytes", bytes.len()),
502                    None => "absent".to_string(),
503                }
504            ),
505        }
506    }
507}
508
509/// Lowercase hex of a 32-byte digest.
510pub fn hash_hex(hash: &[u8; 32]) -> String {
511    let mut out = String::with_capacity(64);
512    for byte in hash {
513        out.push_str(&format!("{byte:02x}"));
514    }
515    out
516}
517
518/// Parse a 64-character hex digest.
519pub fn parse_hash_hex(text: &str) -> Option<[u8; 32]> {
520    if text.len() != 64 {
521        return None;
522    }
523    let mut out = [0u8; 32];
524    for (index, pair) in text.as_bytes().chunks_exact(2).enumerate() {
525        let hi = (pair[0] as char).to_digit(16)?;
526        let lo = (pair[1] as char).to_digit(16)?;
527        out[index] = (hi * 16 + lo) as u8;
528    }
529    Some(out)
530}
531
532/// Stable node-kind identity, usable without depending on wire ids.
533///
534/// Downstream tools (the RPC layer, projections, diagnostics) name kinds through
535/// this enum; `code()` is the permanent wire discriminant.
536#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, PartialOrd, Ord)]
537#[allow(missing_docs)]
538pub enum SerializedKind {
539    Null,
540    Bool,
541    Int,
542    Float,
543    Str,
544    DurationMs,
545    Quantity,
546    Array,
547    Table,
548    Secret,
549    SecretRef,
550    Ref,
551    Env,
552    Ident,
553    Interpolate,
554    Binary,
555    Unary,
556    Constrained,
557    Patch,
558    SchemaField,
559    SchemaTable,
560    SchemaArray,
561    Semantic,
562    Blob,
563    ContentAddressed,
564}
565
566impl SerializedKind {
567    pub const fn code(self) -> u8 {
568        match self {
569            SerializedKind::Null => 0,
570            SerializedKind::Bool => 1,
571            SerializedKind::Int => 2,
572            SerializedKind::Float => 3,
573            SerializedKind::Str => 4,
574            SerializedKind::DurationMs => 5,
575            SerializedKind::Quantity => 6,
576            SerializedKind::Array => 7,
577            SerializedKind::Table => 8,
578            SerializedKind::Secret => 9,
579            SerializedKind::SecretRef => 10,
580            SerializedKind::Ref => 11,
581            SerializedKind::Env => 12,
582            SerializedKind::Ident => 13,
583            SerializedKind::Interpolate => 14,
584            SerializedKind::Binary => 15,
585            SerializedKind::Unary => 16,
586            SerializedKind::Constrained => 17,
587            SerializedKind::Patch => 18,
588            SerializedKind::SchemaField => 19,
589            SerializedKind::SchemaTable => 20,
590            SerializedKind::SchemaArray => 21,
591            SerializedKind::Semantic => 22,
592            SerializedKind::Blob => 23,
593            SerializedKind::ContentAddressed => 24,
594        }
595    }
596
597    pub const fn from_code(code: u8) -> Option<Self> {
598        let kind = match code {
599            0 => SerializedKind::Null,
600            1 => SerializedKind::Bool,
601            2 => SerializedKind::Int,
602            3 => SerializedKind::Float,
603            4 => SerializedKind::Str,
604            5 => SerializedKind::DurationMs,
605            6 => SerializedKind::Quantity,
606            7 => SerializedKind::Array,
607            8 => SerializedKind::Table,
608            9 => SerializedKind::Secret,
609            10 => SerializedKind::SecretRef,
610            11 => SerializedKind::Ref,
611            12 => SerializedKind::Env,
612            13 => SerializedKind::Ident,
613            14 => SerializedKind::Interpolate,
614            15 => SerializedKind::Binary,
615            16 => SerializedKind::Unary,
616            17 => SerializedKind::Constrained,
617            18 => SerializedKind::Patch,
618            19 => SerializedKind::SchemaField,
619            20 => SerializedKind::SchemaTable,
620            21 => SerializedKind::SchemaArray,
621            22 => SerializedKind::Semantic,
622            23 => SerializedKind::Blob,
623            24 => SerializedKind::ContentAddressed,
624            _ => return None,
625        };
626        Some(kind)
627    }
628
629    pub const fn name(self) -> &'static str {
630        crate::view::node_kind_name(self.code())
631    }
632}
633
634impl Inode {
635    /// Stable kind identity of this node.
636    pub const fn kind(&self) -> SerializedKind {
637        match self {
638            Inode::Null => SerializedKind::Null,
639            Inode::Bool(_) => SerializedKind::Bool,
640            Inode::Int(_) => SerializedKind::Int,
641            Inode::Float(_) => SerializedKind::Float,
642            Inode::Str(_) => SerializedKind::Str,
643            Inode::DurationMs(_) => SerializedKind::DurationMs,
644            Inode::Quantity { .. } => SerializedKind::Quantity,
645            Inode::Array { .. } => SerializedKind::Array,
646            Inode::Table { .. } => SerializedKind::Table,
647            Inode::Secret(_) => SerializedKind::Secret,
648            Inode::SecretRef { .. } => SerializedKind::SecretRef,
649            Inode::Ref(_) => SerializedKind::Ref,
650            Inode::Env { .. } => SerializedKind::Env,
651            Inode::Ident(_) => SerializedKind::Ident,
652            Inode::Interpolate { .. } => SerializedKind::Interpolate,
653            Inode::Binary { .. } => SerializedKind::Binary,
654            Inode::Unary { .. } => SerializedKind::Unary,
655            Inode::Constrained { .. } => SerializedKind::Constrained,
656            Inode::Patch(_) => SerializedKind::Patch,
657            Inode::SchemaField(_) => SerializedKind::SchemaField,
658            Inode::SchemaTable { .. } => SerializedKind::SchemaTable,
659            Inode::SchemaArray { .. } => SerializedKind::SchemaArray,
660            Inode::Semantic { .. } => SerializedKind::Semantic,
661            Inode::Blob { .. } => SerializedKind::Blob,
662            Inode::ContentAddressed { .. } => SerializedKind::ContentAddressed,
663        }
664    }
665}
666
667impl Inode {
668    /// Stable kind discriminant (wire contract, `docs/spec/layout.md` §5.1).
669    pub const fn kind_code(&self) -> u8 {
670        match self {
671            Inode::Null => 0,
672            Inode::Bool(_) => 1,
673            Inode::Int(_) => 2,
674            Inode::Float(_) => 3,
675            Inode::Str(_) => 4,
676            Inode::DurationMs(_) => 5,
677            Inode::Quantity { .. } => 6,
678            Inode::Array { .. } => 7,
679            Inode::Table { .. } => 8,
680            Inode::Secret(_) => 9,
681            Inode::SecretRef { .. } => 10,
682            Inode::Ref(_) => 11,
683            Inode::Env { .. } => 12,
684            Inode::Ident(_) => 13,
685            Inode::Interpolate { .. } => 14,
686            Inode::Binary { .. } => 15,
687            Inode::Unary { .. } => 16,
688            Inode::Constrained { .. } => 17,
689            Inode::Patch(_) => 18,
690            Inode::SchemaField(_) => 19,
691            Inode::SchemaTable { .. } => 20,
692            Inode::SchemaArray { .. } => 21,
693            Inode::Semantic { .. } => 22,
694            Inode::Blob { .. } => 23,
695            Inode::ContentAddressed { .. } => 24,
696        }
697    }
698
699    /// Whether this node carries secret material or a secret locator.
700    pub const fn is_secret(&self) -> bool {
701        matches!(self, Inode::Secret(_) | Inode::SecretRef { .. })
702    }
703
704    /// Node references in canonical child order.
705    ///
706    /// This is the single source of truth for the dependency edges, the
707    /// canonical numbering traversal, cycle detection and graph queries. It
708    /// must list *every* node reference the variant holds.
709    pub fn children(&self) -> Vec<NodeId> {
710        match self {
711            Inode::Quantity { value, .. } => vec![*value],
712            Inode::Array { elements } => elements.clone(),
713            Inode::Table { fields } => fields.iter().map(|(_, id)| *id).collect(),
714            Inode::SecretRef { locator } => vec![*locator],
715            Inode::Ref(id) => vec![*id],
716            Inode::Env { default, .. } => default.iter().copied().collect(),
717            Inode::Interpolate { parts } => parts
718                .iter()
719                .filter_map(|part| match part {
720                    InterpPart::Expr(id) => Some(*id),
721                    InterpPart::Literal(_) => None,
722                })
723                .collect(),
724            Inode::Binary { lhs, rhs, .. } => vec![*lhs, *rhs],
725            Inode::Unary { expr, .. } => vec![*expr],
726            Inode::Constrained { target, .. } => vec![*target],
727            Inode::Patch(op) => match op {
728                PatchOp::Set { value, .. } => vec![*value],
729                PatchOp::Remove { .. } | PatchOp::Rename { .. } => Vec::new(),
730            },
731            Inode::SchemaField(spec) => {
732                let mut out = Vec::new();
733                for id in [
734                    spec.min,
735                    spec.max,
736                    spec.min_len,
737                    spec.max_len,
738                    spec.pattern,
739                    spec.default,
740                ]
741                .into_iter()
742                .flatten()
743                {
744                    out.push(id);
745                }
746                out
747            }
748            Inode::SchemaTable { fields, .. } => fields.iter().map(|(_, id)| *id).collect(),
749            Inode::SchemaArray { element } => vec![*element],
750            Inode::Semantic { inner, .. } => vec![*inner],
751            Inode::ContentAddressed { media_type, data } => vec![*media_type, *data],
752            Inode::Null
753            | Inode::Bool(_)
754            | Inode::Int(_)
755            | Inode::Float(_)
756            | Inode::Str(_)
757            | Inode::DurationMs(_)
758            | Inode::Secret(_)
759            | Inode::Ident(_)
760            | Inode::Blob { .. } => Vec::new(),
761        }
762    }
763
764    /// Every string this node owns, for heap construction.
765    pub fn strings(&self, out: &mut Vec<String>) {
766        match self {
767            Inode::Str(s) | Inode::Ident(s) => out.push(s.clone()),
768            Inode::Table { fields } => {
769                for (name, _) in fields {
770                    out.push(name.clone());
771                }
772            }
773            Inode::Env { name, .. } => out.push(name.clone()),
774            Inode::Interpolate { parts } => {
775                for part in parts {
776                    if let InterpPart::Literal(s) = part {
777                        out.push(s.clone());
778                    }
779                }
780            }
781            Inode::Constrained { constraint, .. } => match constraint {
782                Constraint::Enum(items) => out.extend(items.iter().cloned()),
783                Constraint::Pattern(s) | Constraint::Custom(s) => out.push(s.clone()),
784                Constraint::MinMax { .. } | Constraint::Odd => {}
785            },
786            Inode::Patch(op) => match op {
787                PatchOp::Set { path, .. } | PatchOp::Remove { path } => out.push(path.clone()),
788                PatchOp::Rename { from, to } => {
789                    out.push(from.clone());
790                    out.push(to.clone());
791                }
792            },
793            Inode::SchemaField(spec) => {
794                if let TypeSpec::Enum(items) = &spec.typ {
795                    out.extend(items.iter().cloned());
796                }
797                if let Some(deprecated) = &spec.deprecated {
798                    out.push(deprecated.clone());
799                }
800            }
801            Inode::SchemaTable { fields, .. } => {
802                for (name, _) in fields {
803                    out.push(name.clone());
804                }
805            }
806            Inode::Blob { media_type, .. } => out.push(media_type.clone()),
807            Inode::Null
808            | Inode::Bool(_)
809            | Inode::Int(_)
810            | Inode::Float(_)
811            | Inode::DurationMs(_)
812            | Inode::Quantity { .. }
813            | Inode::Array { .. }
814            | Inode::Secret(_)
815            | Inode::SecretRef { .. }
816            | Inode::Ref(_)
817            | Inode::Unary { .. }
818            | Inode::Binary { .. }
819            | Inode::SchemaArray { .. }
820            | Inode::Semantic { .. }
821            | Inode::ContentAddressed { .. } => {}
822        }
823    }
824}
825
826/// Annotation kinds (spec/annotations.md §2–§4).
827#[derive(Debug, Clone, PartialEq)]
828pub enum AnnotationKind {
829    /// Documentation only, never enforced.
830    Intent(String),
831    /// A constraint predicate attached to a node.
832    Constraint { source: String, expr: NodeId },
833    /// Agent-immutable flag.
834    AiImmutable(bool),
835}
836
837/// One annotation attached to a node.
838#[derive(Debug, Clone, PartialEq)]
839pub struct Annotation {
840    pub node: NodeId,
841    pub kind: AnnotationKind,
842}
843
844/// Kinds of provenance entries.
845#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
846pub enum ProvKind {
847    Text,
848    Int,
849    Flag,
850    Hash,
851    /// Input module content hash (delta/derivation lineage).
852    Input,
853}
854
855impl ProvKind {
856    pub const fn code(self) -> u32 {
857        match self {
858            ProvKind::Text => 0,
859            ProvKind::Int => 1,
860            ProvKind::Flag => 2,
861            ProvKind::Hash => 3,
862            ProvKind::Input => 4,
863        }
864    }
865
866    pub const fn from_code(code: u32) -> Option<Self> {
867        match code {
868            0 => Some(ProvKind::Text),
869            1 => Some(ProvKind::Int),
870            2 => Some(ProvKind::Flag),
871            3 => Some(ProvKind::Hash),
872            4 => Some(ProvKind::Input),
873            _ => None,
874        }
875    }
876}
877
878/// A provenance value.
879#[derive(Debug, Clone, PartialEq)]
880pub enum ProvValue {
881    Text(String),
882    Int(i64),
883    Flag(bool),
884    Hash([u8; 32]),
885}
886
887/// One provenance entry: a typed key/value pair.
888#[derive(Debug, Clone, PartialEq)]
889pub struct ProvEntry {
890    pub key: String,
891    pub value: ProvValue,
892}
893
894impl ProvEntry {
895    pub fn kind(&self) -> ProvKind {
896        match self.value {
897            ProvValue::Text(_) => ProvKind::Text,
898            ProvValue::Int(_) => ProvKind::Int,
899            ProvValue::Flag(_) => ProvKind::Flag,
900            ProvValue::Hash(_) => ProvKind::Hash,
901        }
902    }
903}
904
905/// Module provenance: who produced this module, when, with what, from what.
906#[derive(Debug, Clone, PartialEq, Default)]
907pub struct Provenance {
908    /// Producer identity (agent, tool, human, pipeline).
909    pub producer: Option<String>,
910    /// Unix epoch milliseconds; `0` means "unrecorded".
911    pub timestamp_ms: u64,
912    /// Model identity when an AI produced the module.
913    pub model: Option<String>,
914    /// Tool identity.
915    pub tool: Option<String>,
916    /// Content hash of the parent module this one derives from.
917    pub parent: Option<[u8; 32]>,
918    /// Free-form typed entries, sorted by key in canonical form.
919    pub entries: Vec<ProvEntry>,
920}
921
922/// A canonical Active Infinity module.
923///
924/// The struct is an arena of [`Inode`] plus roots, bindings, annotations and
925/// provenance. Edges are *derived* from node children, so a module can never
926/// hold an edge table inconsistent with its nodes.
927#[derive(Debug, Clone, Default)]
928pub struct InfinityModule {
929    nodes: Vec<Inode>,
930    root: Option<NodeId>,
931    bindings: Vec<(String, NodeId)>,
932    annotations: Vec<Annotation>,
933    provenance: Option<Provenance>,
934    extensions: Vec<(u64, Vec<u8>)>,
935}
936
937impl PartialEq for InfinityModule {
938    /// Canonical structural equality: two modules are equal when their
939    /// canonical forms match, so arena order, table field order and blob
940    /// numbering are irrelevant.
941    ///
942    /// Secret *contents* are never compared — only the fact that a secret is
943    /// there — so equality can never become a guessing oracle, and a module
944    /// holding a secret is equal to a structurally identical copy of itself.
945    /// Use [`SecretBytes`] directly when you need the anti-oracle behaviour of
946    /// two secret values.
947    fn eq(&self, other: &Self) -> bool {
948        self.structurally_eq(other)
949    }
950}
951
952/// Compare two canonical nodes, blinding secret payloads.
953fn node_eq(left: &Inode, right: &Inode) -> bool {
954    match (left, right) {
955        (Inode::Secret(_), Inode::Secret(_)) => true,
956        (Inode::Secret(_), _) | (_, Inode::Secret(_)) => false,
957        // Canonicalization normalizes floats; compare their bits so a NaN is
958        // equal to itself.
959        (Inode::Float(a), Inode::Float(b)) => a.to_bits() == b.to_bits(),
960        _ => left == right,
961    }
962}
963
964impl InfinityModule {
965    pub fn new() -> Self {
966        Self::default()
967    }
968
969    /// Add a node, returning its id.
970    pub fn push(&mut self, node: Inode) -> NodeId {
971        let id = NodeId(u32::try_from(self.nodes.len()).expect("arena exceeds u32 ids"));
972        self.nodes.push(node);
973        id
974    }
975
976    /// Alias for [`InfinityModule::push`], mirroring `Module::add_node`.
977    pub fn add_node(&mut self, node: Inode) -> NodeId {
978        self.push(node)
979    }
980
981    pub fn get(&self, id: NodeId) -> Option<&Inode> {
982        self.nodes.get(id.index())
983    }
984
985    pub fn get_mut(&mut self, id: NodeId) -> Option<&mut Inode> {
986        self.nodes.get_mut(id.index())
987    }
988
989    pub fn node_mut(&mut self, id: NodeId) -> Result<&mut Inode, super::Error> {
990        self.nodes
991            .get_mut(id.index())
992            .ok_or(super::Error::NodeRef(id.0))
993    }
994
995    pub fn nodes(&self) -> &[Inode] {
996        &self.nodes
997    }
998
999    pub fn node_count(&self) -> usize {
1000        self.nodes.len()
1001    }
1002
1003    pub fn is_empty(&self) -> bool {
1004        self.nodes.is_empty()
1005    }
1006
1007    pub fn root(&self) -> Option<NodeId> {
1008        self.root
1009    }
1010
1011    pub fn set_root(&mut self, root: Option<NodeId>) {
1012        self.root = root;
1013    }
1014
1015    pub fn bindings(&self) -> &[(String, NodeId)] {
1016        &self.bindings
1017    }
1018
1019    /// Bind a name to a node. Replaces an existing binding with the same name.
1020    pub fn bind(&mut self, name: impl Into<String>, id: NodeId) {
1021        let name = name.into();
1022        if let Some(slot) = self.bindings.iter_mut().find(|(n, _)| *n == name) {
1023            slot.1 = id;
1024        } else {
1025            self.bindings.push((name, id));
1026        }
1027    }
1028
1029    pub fn annotations(&self) -> &[Annotation] {
1030        &self.annotations
1031    }
1032
1033    pub fn annotate(&mut self, node: NodeId, kind: AnnotationKind) {
1034        self.annotations.push(Annotation { node, kind });
1035    }
1036
1037    /// Drop every annotation.
1038    pub fn clear_annotations(&mut self) {
1039        self.annotations.clear();
1040    }
1041
1042    /// Remove a binding by name; returns the old target when present.
1043    pub fn remove_binding(&mut self, name: &str) -> Option<NodeId> {
1044        let position = self.bindings.iter().position(|(key, _)| key == name)?;
1045        Some(self.bindings.remove(position).1)
1046    }
1047
1048    pub fn provenance(&self) -> Option<&Provenance> {
1049        self.provenance.as_ref()
1050    }
1051
1052    /// True when the module holds a structural secret literal.
1053    pub fn has_secret(&self) -> bool {
1054        self.nodes
1055            .iter()
1056            .any(|node| matches!(node, Inode::Secret(_)))
1057    }
1058
1059    /// Canonical structural equality, blinding secret payloads.
1060    ///
1061    /// This is what [`PartialEq`] implements; it is also available by name for
1062    /// readers who want the intent spelled out.
1063    pub fn structurally_eq(&self, other: &Self) -> bool {
1064        let (Ok(left), Ok(right)) = (
1065            crate::canonical::prepare(self),
1066            crate::canonical::prepare(other),
1067        ) else {
1068            return false;
1069        };
1070        left.root == right.root
1071            && left.bindings == right.bindings
1072            && left.annotations == right.annotations
1073            && left.provenance == right.provenance
1074            && left.strings == right.strings
1075            && left.units == right.units
1076            && left.blobs == right.blobs
1077            && left.hashes == right.hashes
1078            && left.numbers == right.numbers
1079            && left.secrets.len() == right.secrets.len()
1080            && left.types.len() == right.types.len()
1081            && left
1082                .types
1083                .iter()
1084                .zip(right.types.iter())
1085                .all(|(a, b)| a.spec == b.spec && a.aux == b.aux)
1086            && left.nodes.len() == right.nodes.len()
1087            && left
1088                .nodes
1089                .iter()
1090                .zip(right.nodes.iter())
1091                .all(|(a, b)| node_eq(a, b))
1092    }
1093
1094    /// Append an extension block (tag, payload).
1095    ///
1096    /// Tags are unique; the canonical encoder sorts blocks by tag and refuses a
1097    /// duplicate. Extension payloads are opaque bytes owned by the application;
1098    /// the codec never interprets them.
1099    pub fn add_extension(&mut self, tag: u64, payload: impl Into<Vec<u8>>) {
1100        self.extensions.push((tag, payload.into()));
1101    }
1102
1103    /// Extension blocks, in stored order.
1104    pub fn extensions(&self) -> &[(u64, Vec<u8>)] {
1105        &self.extensions
1106    }
1107
1108    /// Replace the extension set.
1109    pub fn set_extensions(&mut self, extensions: Vec<(u64, Vec<u8>)>) {
1110        self.extensions = extensions;
1111    }
1112
1113    pub fn set_provenance(&mut self, provenance: Option<Provenance>) {
1114        // Provenance is a map keyed by name: the last value for a key wins, so
1115        // the stored form can never contain the duplicate keys that validation
1116        // (correctly) rejects. Entries stay sorted for canonical equality.
1117        self.provenance = provenance.map(|prov| {
1118            let mut by_key: std::collections::BTreeMap<String, ProvValue> =
1119                std::collections::BTreeMap::new();
1120            for entry in prov.entries {
1121                by_key.insert(entry.key, entry.value);
1122            }
1123            let entries: Vec<ProvEntry> = by_key
1124                .into_iter()
1125                .map(|(key, value)| ProvEntry { key, value })
1126                .collect();
1127            Provenance { entries, ..prov }
1128        });
1129    }
1130
1131    /// Roots used to define the reachable graph: the module root, every
1132    /// binding, every annotation subject, and provenance-only references.
1133    pub fn roots(&self) -> Vec<NodeId> {
1134        let mut roots = Vec::new();
1135        if let Some(root) = self.root {
1136            roots.push(root);
1137        }
1138        let mut bindings: Vec<&(String, NodeId)> = self.bindings.iter().collect();
1139        bindings.sort_by(|a, b| a.0.cmp(&b.0));
1140        roots.extend(bindings.into_iter().map(|(_, id)| *id));
1141        for annotation in &self.annotations {
1142            roots.push(annotation.node);
1143            if let AnnotationKind::Constraint { expr, .. } = &annotation.kind {
1144                roots.push(*expr);
1145            }
1146        }
1147        roots
1148    }
1149
1150    /// Derived dependency edges, direction dependency → dependent, sorted and
1151    /// deduplicated. Nodes that reference where they are referenced from are
1152    /// normal; the graph is a DAG after [`Self::validate_structure`].
1153    pub fn edges(&self) -> Vec<(NodeId, NodeId)> {
1154        let mut edges = Vec::new();
1155        for (index, node) in self.nodes.iter().enumerate() {
1156            let dependent = NodeId(index as u32);
1157            for child in node.children() {
1158                edges.push((child, dependent));
1159            }
1160        }
1161        edges.sort_unstable();
1162        edges.dedup();
1163        edges
1164    }
1165
1166    /// Reverse adjacency (dependent → dependencies).
1167    pub fn dependency_map(&self) -> HashMap<NodeId, Vec<NodeId>> {
1168        let mut map: HashMap<NodeId, Vec<NodeId>> = HashMap::new();
1169        for (index, node) in self.nodes.iter().enumerate() {
1170            map.insert(NodeId(index as u32), node.children());
1171        }
1172        map
1173    }
1174
1175    /// Check every structural invariant the wire format also enforces.
1176    ///
1177    /// `max_nodes` and `max_depth` bound the work so a hostile module cannot
1178    /// make this function hang or recurse without limit.
1179    pub fn validate_structure(&self, max_nodes: u64, max_depth: u32) -> Result<(), super::Error> {
1180        use super::Error;
1181        if self.nodes.len() as u64 > max_nodes {
1182            return Err(Error::Limit {
1183                kind: "nodes",
1184                limit: max_nodes,
1185            });
1186        }
1187        let count = self.nodes.len() as u32;
1188        let check = |id: NodeId| -> Result<(), Error> {
1189            if id.0 >= count {
1190                return Err(Error::NodeRef(id.0));
1191            }
1192            Ok(())
1193        };
1194        for (index, node) in self.nodes.iter().enumerate() {
1195            let id = NodeId(index as u32);
1196            for child in node.children() {
1197                check(child)?;
1198            }
1199            match node {
1200                Inode::Table { fields } | Inode::SchemaTable { fields, .. } => {
1201                    // O(k) duplicate detection: a 256-field table is common and
1202                    // the quadratic form dominated encoding time.
1203                    let mut seen = std::collections::HashSet::with_capacity(fields.len());
1204                    for (name, _) in fields {
1205                        if !seen.insert(name.as_str()) {
1206                            return Err(Error::DuplicateField { node: id.0 });
1207                        }
1208                    }
1209                }
1210                Inode::Float(value)
1211                    if value.is_nan() && !crate::canonical::is_canonical_nan(*value) =>
1212                {
1213                    return Err(Error::Node {
1214                        id: id.0,
1215                        reason: "non-canonical NaN",
1216                    });
1217                }
1218                Inode::Constrained { constraint, .. } => match constraint {
1219                    Constraint::MinMax {
1220                        min: Some(min),
1221                        max: Some(max),
1222                    } if min > max => {
1223                        return Err(Error::Node {
1224                            id: id.0,
1225                            reason: "constraint min exceeds max",
1226                        });
1227                    }
1228                    Constraint::Enum(items) if items.is_empty() => {
1229                        return Err(Error::Node {
1230                            id: id.0,
1231                            reason: "empty enum constraint",
1232                        });
1233                    }
1234                    _ => {}
1235                },
1236                _ => {}
1237            }
1238        }
1239        if let Some(root) = self.root {
1240            check(root)?;
1241        }
1242        let mut seen = std::collections::HashSet::new();
1243        for (name, id) in &self.bindings {
1244            check(*id)?;
1245            if !seen.insert(name.as_str()) {
1246                return Err(Error::DuplicateBinding);
1247            }
1248        }
1249        for annotation in &self.annotations {
1250            check(annotation.node)?;
1251            if let AnnotationKind::Constraint { expr, .. } = &annotation.kind {
1252                check(*expr)?;
1253            }
1254        }
1255        self.check_acyclic(count)?;
1256        self.check_depth(max_depth)?;
1257        Ok(())
1258    }
1259
1260    /// Cycle detection over the derived dependency graph (Kahn sweep).
1261    ///
1262    /// One child-list derivation per node, so a wide table stays linear; the
1263    /// sweep is iterative by construction, because a hostile 4M-node chain must
1264    /// not overflow the thread stack (`active-core` treats cycles as fatal, so
1265    /// this is a hard invariant, not a hint).
1266    pub fn check_acyclic(&self, count: u32) -> Result<(), super::Error> {
1267        use super::Error;
1268        let mut incoming = vec![0u32; count as usize];
1269        for node in &self.nodes {
1270            for child in node.children() {
1271                if child.0 >= count {
1272                    return Err(Error::NodeRef(child.0));
1273                }
1274                let slot = &mut incoming[child.index()];
1275                *slot = slot.saturating_add(1);
1276            }
1277        }
1278        let mut queue: std::collections::VecDeque<u32> = incoming
1279            .iter()
1280            .enumerate()
1281            .filter(|(_, degree)| **degree == 0)
1282            .map(|(node, _)| node as u32)
1283            .collect();
1284        let mut processed = 0u32;
1285        while let Some(node) = queue.pop_front() {
1286            processed += 1;
1287            for child in self.nodes[node as usize].children() {
1288                let slot = &mut incoming[child.index()];
1289                *slot = slot.saturating_sub(1);
1290                if *slot == 0 {
1291                    queue.push_back(child.0);
1292                }
1293            }
1294        }
1295        if processed != count {
1296            // A node that could not be ordered depends on a cycle; report the
1297            // lowest one so the answer is stable.
1298            let node = incoming.iter().position(|degree| *degree > 0).unwrap_or(0) as u32;
1299            return Err(Error::Cycle { node });
1300        }
1301        Ok(())
1302    }
1303
1304    /// Depth check over the derived dependency graph (topological sweep).
1305    ///
1306    /// Each edge is examined exactly once, so a wide or diamond-shaped graph is
1307    /// linear rather than polynomial.
1308    pub fn check_depth(&self, max_depth: u32) -> Result<(), super::Error> {
1309        use super::Error;
1310        let count = self.nodes.len();
1311        if count == 0 {
1312            return Ok(());
1313        }
1314        if max_depth == 0 {
1315            // Zero means "no depth ceiling", not "no checks": a direct caller
1316            // still gets cycle detection.
1317            return self.check_acyclic(count as u32);
1318        }
1319        let mut incoming = vec![0u32; count];
1320        for node in &self.nodes {
1321            for child in node.children() {
1322                if child.index() >= count {
1323                    return Err(Error::NodeRef(child.0));
1324                }
1325                incoming[child.index()] = incoming[child.index()].saturating_add(1);
1326            }
1327        }
1328        let mut depth = vec![1u32; count];
1329        let mut queue: std::collections::VecDeque<u32> = incoming
1330            .iter()
1331            .enumerate()
1332            .filter(|(_, degree)| **degree == 0)
1333            .map(|(node, _)| node as u32)
1334            .collect();
1335        if queue.is_empty() {
1336            // Acyclicity runs first; stay total for a direct caller.
1337            return Err(Error::Cycle { node: 0 });
1338        }
1339        let mut processed = 0u32;
1340        while let Some(node) = queue.pop_front() {
1341            processed += 1;
1342            let next = depth[node as usize] + 1;
1343            for child in self.nodes[node as usize].children() {
1344                if next > max_depth {
1345                    return Err(Error::Depth { limit: max_depth });
1346                }
1347                if next > depth[child.index()] {
1348                    depth[child.index()] = next;
1349                }
1350                let slot = &mut incoming[child.index()];
1351                *slot = slot.saturating_sub(1);
1352                if *slot == 0 {
1353                    queue.push_back(child.0);
1354                }
1355            }
1356        }
1357        if processed as usize != count {
1358            // Acyclicity runs first, so this is defensive; report the lowest
1359            // node that could not be ordered, like `check_acyclic`.
1360            let node = incoming.iter().position(|degree| *degree > 0).unwrap_or(0) as u32;
1361            return Err(Error::Cycle { node });
1362        }
1363        Ok(())
1364    }
1365
1366    /// Total string payload bytes (heap size before deduplication).
1367    pub fn string_bytes(&self) -> u64 {
1368        let mut strings = Vec::new();
1369        for node in &self.nodes {
1370            node.strings(&mut strings);
1371        }
1372        strings.iter().map(|s| s.len() as u64).sum()
1373    }
1374
1375    /// Rebuild a module from decoded parts (used by the decoder and the
1376    /// stream reassembler).
1377    pub fn from_parts(
1378        nodes: Vec<Inode>,
1379        root: Option<NodeId>,
1380        bindings: Vec<(String, NodeId)>,
1381        mut annotations: Vec<Annotation>,
1382        provenance: Option<Provenance>,
1383    ) -> Self {
1384        annotations.sort_by(|a, b| {
1385            (a.node.0, annotation_order(&a.kind)).cmp(&(b.node.0, annotation_order(&b.kind)))
1386        });
1387        InfinityModule {
1388            nodes,
1389            root,
1390            bindings,
1391            annotations,
1392            provenance,
1393            extensions: Vec::new(),
1394        }
1395    }
1396}
1397
1398fn annotation_order(kind: &AnnotationKind) -> u32 {
1399    match kind {
1400        AnnotationKind::Intent(_) => 0,
1401        AnnotationKind::Constraint { .. } => 1,
1402        AnnotationKind::AiImmutable(_) => 2,
1403    }
1404}
1405
1406/// Builder helpers that keep the node graph consistent while constructing a
1407/// module by hand or from another IR.
1408#[derive(Debug, Default)]
1409pub struct ModuleBuilder {
1410    module: InfinityModule,
1411}
1412
1413impl ModuleBuilder {
1414    pub fn new() -> Self {
1415        Self::default()
1416    }
1417
1418    pub fn push(&mut self, node: Inode) -> NodeId {
1419        self.module.push(node)
1420    }
1421
1422    pub fn root(&mut self, id: NodeId) -> &mut Self {
1423        self.module.set_root(Some(id));
1424        self
1425    }
1426
1427    pub fn bind(&mut self, name: impl Into<String>, id: NodeId) -> &mut Self {
1428        self.module.bind(name, id);
1429        self
1430    }
1431
1432    pub fn annotate(&mut self, node: NodeId, kind: AnnotationKind) -> &mut Self {
1433        self.module.annotate(node, kind);
1434        self
1435    }
1436
1437    pub fn provenance(&mut self, provenance: Provenance) -> &mut Self {
1438        self.module.set_provenance(Some(provenance));
1439        self
1440    }
1441
1442    pub fn build(self) -> InfinityModule {
1443        self.module
1444    }
1445}