Skip to main content

blut_graph_core/
model.rs

1// SPDX-License-Identifier: AGPL-3.0-or-later
2
3use alloc::collections::BTreeMap;
4use alloc::string::String;
5use alloc::vec::Vec;
6use serde::{Deserialize, Serialize};
7
8use crate::config::{ConfigSchema, ConfigValue};
9
10macro_rules! id_type {
11    ($name:ident) => {
12        #[derive(
13            Clone, Copy, Debug, PartialEq, Eq, PartialOrd, Ord, Hash, Serialize, Deserialize,
14        )]
15        #[serde(transparent)]
16        pub struct $name(pub u32);
17    };
18}
19
20id_type!(NodeId);
21id_type!(KernelId);
22id_type!(BufferId);
23id_type!(StepId);
24id_type!(FeedbackId);
25
26#[derive(Clone, Copy, Debug, PartialEq, Eq, Hash, Serialize, Deserialize)]
27#[serde(transparent)]
28pub struct GraphId(pub [u8; 32]);
29
30#[derive(Clone, Copy, Debug, PartialEq, Eq, Hash, Serialize, Deserialize)]
31#[serde(transparent)]
32pub struct PlanId(pub [u8; 32]);
33
34#[derive(Clone, Copy, Debug, PartialEq, Eq, PartialOrd, Ord, Hash, Serialize, Deserialize)]
35#[serde(transparent)]
36pub struct SubgraphId(pub [u8; 32]);
37
38#[derive(Clone, Copy, Debug, PartialEq, Eq, PartialOrd, Ord, Hash, Serialize, Deserialize)]
39#[serde(transparent)]
40pub struct ImplementationId(pub [u8; 32]);
41
42#[derive(Clone, Debug, PartialEq, Eq, PartialOrd, Ord, Hash, Serialize, Deserialize)]
43pub struct NodeTypeRef {
44    pub type_name: String,
45    pub version: u32,
46}
47
48#[derive(Clone, Debug, PartialEq, Eq, PartialOrd, Ord, Serialize, Deserialize)]
49#[serde(transparent)]
50pub struct Capability(pub String);
51
52/// An opaque execution target token.
53///
54/// This was an enum until 0.3.0, and the change is deliberate: the compiler
55/// has no business knowing that a target is called "host". It compares tokens,
56/// orders them, and folds them into the plan hash — nothing more. The domain
57/// layer names them (ADR 0034), exactly as it already names `DomainToken`.
58///
59/// THE ORDINALS ARE WIRE. They are folded into `graph_id` and `plan_id` as
60/// little-endian `u32`, and `#[serde(transparent)]` makes the postcard encoding
61/// byte-identical to the variant indices the enum emitted. The values below are
62/// therefore historically assigned and may never be renumbered.
63///
64/// `Debug` is wire too, and that is less obvious: `KernelDescriptor::lowering`
65/// is conventionally built as `format!("{target:?}")`, and `lowering` is a
66/// hashed field. The hand-written `Debug` below reproduces the enum's output
67/// exactly for that reason; a derived one would print `Target(1)` and move every
68/// plan id in the fleet without touching an ordinal.
69#[derive(Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Hash, Serialize, Deserialize)]
70#[serde(transparent)]
71pub struct Target(u32);
72
73#[allow(non_upper_case_globals)]
74impl Target {
75    /// Historically assigned 0.
76    pub const McuAot: Self = Self(0);
77    /// Historically assigned 1.
78    pub const Host: Self = Self(1);
79    /// Historically assigned 2.
80    pub const BlutDurable: Self = Self(2);
81
82    /// Every token this version of the crate knows, in ordinal order.
83    pub const KNOWN: [Self; 3] = [Self::McuAot, Self::Host, Self::BlutDurable];
84
85    /// The wire value. Named `token` rather than `as u32` so that the cast
86    /// sites are greppable and cannot be written by accident.
87    pub const fn token(self) -> u32 {
88        self.0
89    }
90
91    /// Build a token from a wire value, WITHOUT range checking — decoding
92    /// untrusted bytes must call `is_known` as well. See `is_known`.
93    pub const fn from_token(token: u32) -> Self {
94        Self(token)
95    }
96
97    /// Whether this token is one this version assigns a meaning to.
98    ///
99    /// The enum's derived `Deserialize` used to reject an out-of-range variant
100    /// index for free. A transparent newtype accepts any `u32`, so the check
101    /// that was implicit is explicit here, and `from_aot_bytes` calls it.
102    pub const fn is_known(self) -> bool {
103        self.0 <= 2
104    }
105
106    const fn name(self) -> Option<&'static str> {
107        match self.0 {
108            0 => Some("McuAot"),
109            1 => Some("Host"),
110            2 => Some("BlutDurable"),
111            _ => None,
112        }
113    }
114}
115
116impl core::fmt::Debug for Target {
117    fn fmt(&self, formatter: &mut core::fmt::Formatter<'_>) -> core::fmt::Result {
118        match self.name() {
119            Some(name) => formatter.write_str(name),
120            None => write!(formatter, "Target({})", self.0),
121        }
122    }
123}
124
125/// An opaque execution realm token. See [`Target`] for why this is a newtype,
126/// why the ordinals are wire, and why `Debug` is hand-written.
127#[derive(Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Hash, Serialize, Deserialize)]
128#[serde(transparent)]
129pub struct ExecutionRealm(u32);
130
131#[allow(non_upper_case_globals)]
132impl ExecutionRealm {
133    /// Historically assigned 0.
134    pub const McuAot: Self = Self(0);
135    /// Historically assigned 1.
136    pub const HostStream: Self = Self(1);
137    /// Historically assigned 2.
138    pub const BlutDurable: Self = Self(2);
139
140    /// Every token this version of the crate knows, in ordinal order.
141    pub const KNOWN: [Self; 3] = [Self::McuAot, Self::HostStream, Self::BlutDurable];
142
143    pub const fn token(self) -> u32 {
144        self.0
145    }
146
147    pub const fn from_token(token: u32) -> Self {
148        Self(token)
149    }
150
151    pub const fn is_known(self) -> bool {
152        self.0 <= 2
153    }
154
155    /// The target a realm lowers to.
156    ///
157    /// Kept as a total function on the realm because the compile-side selection
158    /// and the decode-side authorization must not be able to disagree; an
159    /// unknown realm maps to an unknown target rather than to a default, so a
160    /// forged plan cannot borrow the host's target by being out of range.
161    pub const fn target(self) -> Target {
162        match self.0 {
163            0 => Target::McuAot,
164            1 => Target::Host,
165            2 => Target::BlutDurable,
166            other => Target::from_token(other),
167        }
168    }
169
170    const fn name(self) -> Option<&'static str> {
171        match self.0 {
172            0 => Some("McuAot"),
173            1 => Some("HostStream"),
174            2 => Some("BlutDurable"),
175            _ => None,
176        }
177    }
178}
179
180impl core::fmt::Debug for ExecutionRealm {
181    fn fmt(&self, formatter: &mut core::fmt::Formatter<'_>) -> core::fmt::Result {
182        match self.name() {
183            Some(name) => formatter.write_str(name),
184            None => write!(formatter, "ExecutionRealm({})", self.0),
185        }
186    }
187}
188
189#[derive(Clone, Copy, Debug, PartialEq, Eq, PartialOrd, Ord, Serialize, Deserialize)]
190#[serde(rename_all = "kebab-case")]
191pub enum Determinism {
192    BitExact,
193    NumericallyEquivalent,
194    Seeded,
195    Nondeterministic,
196}
197
198#[derive(Clone, Copy, Debug, PartialEq, Eq, PartialOrd, Ord, Serialize, Deserialize)]
199#[serde(rename_all = "kebab-case")]
200pub enum Effect {
201    Pure,
202    Idempotent,
203    Transactional,
204    AtMostOnce,
205    AtLeastOnce,
206}
207
208#[derive(Clone, Copy, Debug, PartialEq, Eq, PartialOrd, Ord, Serialize, Deserialize)]
209#[serde(rename_all = "kebab-case")]
210pub enum Partiality {
211    /// Either all declared outputs are produced or the node fails.
212    Atomic,
213    /// A successful attempt may include explicit, machine-readable gaps.
214    ExplicitGaps,
215}
216
217#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize)]
218pub struct FailureContract {
219    /// Stable namespaced failure domains a kernel is allowed to report.
220    pub domains: Vec<String>,
221}
222
223/// An opaque buffer-layout token. See [`Target`] for why this is a newtype,
224/// why the ordinals are wire, and why `Debug` is hand-written.
225///
226/// `Ord` is load-bearing beyond hashing here: `select_layout` resolves a port's
227/// admissible layouts with `.min()`, so the ordinal order IS the selection
228/// rule, and two further sites sort or compare layouts to break routing ties.
229/// Derived `Ord` over the `u32` preserves all three exactly.
230#[derive(Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Hash, Serialize, Deserialize)]
231#[serde(transparent)]
232pub struct Layout(u32);
233
234#[allow(non_upper_case_globals)]
235impl Layout {
236    /// Historically assigned 0.
237    pub const Canonical: Self = Self(0);
238    /// Historically assigned 1.
239    pub const ChannelMajor: Self = Self(1);
240    /// Historically assigned 2.
241    pub const TimeMajor: Self = Self(2);
242    /// Historically assigned 3.
243    pub const Packed: Self = Self(3);
244    /// Historically assigned 4.
245    pub const Opaque: Self = Self(4);
246
247    /// Every token this version of the crate knows, in ordinal order.
248    pub const KNOWN: [Self; 5] = [
249        Self::Canonical,
250        Self::ChannelMajor,
251        Self::TimeMajor,
252        Self::Packed,
253        Self::Opaque,
254    ];
255
256    pub const fn token(self) -> u32 {
257        self.0
258    }
259
260    pub const fn from_token(token: u32) -> Self {
261        Self(token)
262    }
263
264    pub const fn is_known(self) -> bool {
265        self.0 <= 4
266    }
267
268    const fn name(self) -> Option<&'static str> {
269        match self.0 {
270            0 => Some("Canonical"),
271            1 => Some("ChannelMajor"),
272            2 => Some("TimeMajor"),
273            3 => Some("Packed"),
274            4 => Some("Opaque"),
275            _ => None,
276        }
277    }
278}
279
280impl core::fmt::Debug for Layout {
281    fn fmt(&self, formatter: &mut core::fmt::Formatter<'_>) -> core::fmt::Result {
282        match self.name() {
283            Some(name) => formatter.write_str(name),
284            None => write!(formatter, "Layout({})", self.0),
285        }
286    }
287}
288
289/// An opaque, domain-supplied classification token.
290///
291/// `blut-graph-core` NEVER interprets these. The compiler does exactly three
292/// things with a token: compares it to another for edge compatibility, rejects
293/// it when empty, and folds its bytes into the plan hash. It has no opinion
294/// about what any particular token *means*.
295///
296/// That is the point. The vocabulary belongs to the domain layer (ADR 0034) —
297/// a biosignal domain names recordings and signal blocks, a vision domain names
298/// frames and tensors, and the compiler stays ignorant of both. Before the 2026-08-26 domain-token migration this
299/// slot was a pair of enums (`AbirRootType`/`AbirViewType`) that hard-coded one
300/// domain's taxonomy into the compiler; consumers were already escaping it
301/// through an `Unknown(String)` variant in 10 of 22 call sites, which is the
302/// shape below with extra steps.
303///
304/// Construction is deliberately permissive — an empty token is representable and
305/// is rejected by [`crate::compile`]'s port-contract validation, exactly as the
306/// empty `Unknown("")` was. Validity is the compiler's judgement, not the
307/// constructor's.
308#[derive(Clone, Debug, Default, PartialEq, Eq, PartialOrd, Ord, Serialize, Deserialize)]
309#[serde(transparent)]
310pub struct DomainToken(String);
311
312impl DomainToken {
313    pub fn new(value: impl Into<String>) -> Self {
314        Self(value.into())
315    }
316
317    pub fn as_str(&self) -> &str {
318        &self.0
319    }
320
321    pub fn is_empty(&self) -> bool {
322        self.0.is_empty()
323    }
324}
325
326impl From<&str> for DomainToken {
327    fn from(value: &str) -> Self {
328        Self(value.into())
329    }
330}
331
332impl From<String> for DomainToken {
333    fn from(value: String) -> Self {
334        Self(value)
335    }
336}
337
338/// A port's domain classification: the artifact, and the projection of it.
339///
340/// `root` names the thing that exists; `view` names the way this port looks at
341/// it. The distinction is real and worth keeping structured — a dataset read as
342/// a stream is not the same contract as a dataset read whole — but both sides
343/// are domain vocabulary, so both are opaque [`DomainToken`]s.
344#[derive(Clone, Debug, Default, PartialEq, Eq, PartialOrd, Ord, Serialize, Deserialize)]
345pub struct DomainType {
346    pub root: DomainToken,
347    pub view: DomainToken,
348}
349
350impl DomainType {
351    pub fn new(root: impl Into<DomainToken>, view: impl Into<DomainToken>) -> Self {
352        Self {
353            root: root.into(),
354            view: view.into(),
355        }
356    }
357}
358
359#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize)]
360pub struct ExtentContract {
361    /// Number of logical dimensions. Zero denotes an opaque scalar/blob atom.
362    pub rank: u8,
363    /// Per-dimension upper bounds; exactly `rank` entries.
364    pub maximum_shape: Vec<u64>,
365    pub max_elements: u64,
366    pub ragged: bool,
367    pub sparse: bool,
368}
369
370#[derive(Clone, Copy, Debug, PartialEq, Eq, PartialOrd, Ord, Serialize, Deserialize)]
371#[serde(rename_all = "kebab-case")]
372pub enum LeaseAccess {
373    ReadOnly,
374    ExclusiveWrite,
375}
376
377#[derive(Clone, Copy, Debug, PartialEq, Eq, PartialOrd, Ord, Serialize, Deserialize)]
378#[serde(rename_all = "kebab-case")]
379pub enum LeaseLifetime {
380    Step,
381    Invocation,
382    Session,
383}
384
385#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize)]
386pub struct LeaseContract {
387    pub access: LeaseAccess,
388    pub lifetime: LeaseLifetime,
389    pub zero_copy_permitted: bool,
390    pub contiguous_required: bool,
391}
392
393#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize)]
394pub struct ResourceEnvelope {
395    pub peak_bytes: u64,
396    pub scratch_bytes: u64,
397    pub threads: u16,
398    pub device: Option<String>,
399}
400
401impl ResourceEnvelope {
402    pub const fn bounded(peak_bytes: u64, scratch_bytes: u64, threads: u16) -> Self {
403        Self {
404            peak_bytes,
405            scratch_bytes,
406            threads,
407            device: None,
408        }
409    }
410}
411
412#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize)]
413pub struct PortDescriptor {
414    pub name: String,
415    pub semantic_type: String,
416    pub optional: bool,
417    pub layouts: Vec<Layout>,
418    pub max_bytes: u64,
419    pub domain: DomainType,
420    pub proof: ProofContract,
421    pub policy: PolicyContract,
422    pub fidelity: FidelityContract,
423    pub extent: ExtentContract,
424    pub lease: LeaseContract,
425}
426
427impl PortDescriptor {
428    /// Conservative bounded atom contract useful for non-ABIR control values.
429    pub fn opaque(
430        name: impl Into<String>,
431        semantic_type: impl Into<String>,
432        max_bytes: u64,
433    ) -> Self {
434        Self {
435            name: name.into(),
436            semantic_type: semantic_type.into(),
437            optional: false,
438            layouts: alloc::vec![Layout::Canonical],
439            max_bytes,
440            domain: DomainType {
441                root: DomainToken::new("blob-ref"),
442                view: DomainToken::new("atom"),
443            },
444            proof: ProofContract {
445                requires: Vec::new(),
446                provides: Vec::new(),
447                invalidates: Vec::new(),
448            },
449            policy: PolicyContract {
450                requires: Vec::new(),
451                adds: Vec::new(),
452            },
453            fidelity: FidelityContract {
454                minimum_input: 0,
455                maximum_loss: 0,
456            },
457            extent: ExtentContract {
458                rank: 0,
459                maximum_shape: Vec::new(),
460                max_elements: 1,
461                ragged: false,
462                sparse: false,
463            },
464            lease: LeaseContract {
465                access: LeaseAccess::ReadOnly,
466                lifetime: LeaseLifetime::Invocation,
467                zero_copy_permitted: false,
468                contiguous_required: false,
469            },
470        }
471    }
472}
473
474#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize)]
475pub struct ProofContract {
476    pub requires: Vec<String>,
477    pub provides: Vec<String>,
478    pub invalidates: Vec<String>,
479}
480
481#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize)]
482pub struct PolicyContract {
483    pub requires: Vec<String>,
484    pub adds: Vec<String>,
485}
486
487#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize)]
488pub struct FidelityContract {
489    pub minimum_input: u16,
490    pub maximum_loss: u16,
491}
492
493#[derive(Clone, Copy, Debug, PartialEq, Eq, PartialOrd, Ord, Serialize, Deserialize)]
494#[serde(rename_all = "kebab-case")]
495pub enum StateScope {
496    Stateless,
497    Invocation,
498    Session,
499    Durable,
500}
501
502#[derive(Clone, Copy, Debug, PartialEq, Eq, PartialOrd, Ord, Serialize, Deserialize)]
503#[serde(rename_all = "kebab-case")]
504pub enum CheckpointMode {
505    Disabled,
506    Optional,
507    Required,
508}
509
510#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize)]
511pub struct CheckpointContract {
512    pub mode: CheckpointMode,
513    pub max_snapshot_bytes: u64,
514    pub max_interval_invocations: u32,
515}
516
517#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize)]
518pub struct StateContract {
519    pub scope: StateScope,
520    pub max_bytes: u64,
521    pub checkpoint: CheckpointContract,
522}
523
524impl StateContract {
525    pub const fn stateless() -> Self {
526        Self {
527            scope: StateScope::Stateless,
528            max_bytes: 0,
529            checkpoint: CheckpointContract {
530                mode: CheckpointMode::Disabled,
531                max_snapshot_bytes: 0,
532                max_interval_invocations: 0,
533            },
534        }
535    }
536
537    pub const fn checkpointable(&self) -> bool {
538        !matches!(self.checkpoint.mode, CheckpointMode::Disabled)
539    }
540}
541
542#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize)]
543pub struct SessionContract {
544    pub namespace: String,
545    pub max_concurrent_sessions: u32,
546    pub max_idle_millis: u64,
547    pub reset_on_plan_change: bool,
548}
549
550#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize)]
551pub struct DelayContract {
552    /// Number of completed invocations between write and visibility.
553    pub invocations: u32,
554    pub initial: DelayInitial,
555}
556
557#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize)]
558#[serde(rename_all = "kebab-case")]
559pub enum DelayInitial {
560    Absent,
561    Zeroed,
562    ContentId([u8; 32]),
563}
564
565#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize)]
566pub struct FeedbackEdge {
567    pub from: PortRef,
568    pub to: PortRef,
569    pub delay: DelayContract,
570}
571
572#[derive(Clone, Debug, PartialEq, Eq, PartialOrd, Ord, Serialize, Deserialize)]
573pub struct PortMap {
574    pub outer: String,
575    pub inner: String,
576}
577
578#[derive(Clone, Debug, PartialEq, Eq, PartialOrd, Ord, Serialize, Deserialize)]
579pub struct SubgraphConfigMap {
580    /// Field on the outer descriptor.
581    pub outer: String,
582    /// Local node receiving the bound value.
583    pub node: NodeId,
584    /// Field on the inner node descriptor.
585    pub inner: String,
586}
587
588#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize)]
589pub struct SubgraphLowering {
590    pub subgraph: SubgraphId,
591    pub input_map: Vec<PortMap>,
592    pub output_map: Vec<PortMap>,
593    /// Exact outer-instance configuration bindings into the inner DAG.
594    pub config_map: Vec<SubgraphConfigMap>,
595}
596
597#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize)]
598pub struct SubgraphNode {
599    /// Identity local to the subgraph; repeated node types remain distinct.
600    pub id: NodeId,
601    pub node_type: NodeTypeRef,
602    pub config: BTreeMap<String, ConfigValue>,
603    /// Optional nested decomposition invoked by this local node.
604    pub child: Option<SubgraphId>,
605}
606
607#[derive(Clone, Debug, PartialEq, Eq, PartialOrd, Ord, Serialize, Deserialize)]
608pub struct SubgraphInterfacePort {
609    pub name: String,
610    pub inner: PortRef,
611}
612
613#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize)]
614pub struct SubgraphSchema {
615    pub id: SubgraphId,
616    pub version: u32,
617    pub nodes: Vec<SubgraphNode>,
618    pub edges: Vec<Edge>,
619    pub inputs: Vec<SubgraphInterfacePort>,
620    pub outputs: Vec<SubgraphInterfacePort>,
621}
622
623#[derive(Clone, Debug, PartialEq, Eq)]
624pub struct MaterializedSubgraph {
625    pub graph: Graph,
626    /// Outer input names bound to concrete inner ports.
627    pub inputs: Vec<SubgraphInterfacePort>,
628    /// Outer output names bound to concrete inner ports.
629    pub outputs: Vec<SubgraphInterfacePort>,
630}
631
632#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize)]
633pub struct NodeDescriptor {
634    pub type_name: String,
635    pub version: u32,
636    pub inputs: Vec<PortDescriptor>,
637    pub outputs: Vec<PortDescriptor>,
638    pub capabilities: Vec<Capability>,
639    pub targets: Vec<Target>,
640    pub resources: ResourceEnvelope,
641    pub determinism: Determinism,
642    pub config: ConfigSchema,
643    pub state: StateContract,
644    pub subgraph: Option<SubgraphLowering>,
645    pub proof: ProofContract,
646    pub policy: PolicyContract,
647    pub fidelity: FidelityContract,
648    pub partiality: Partiality,
649    pub failure: FailureContract,
650    pub effect: Effect,
651    pub retry_limit: u16,
652}
653
654#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize)]
655pub struct NodeInstance {
656    pub id: NodeId,
657    pub descriptor: String,
658    pub descriptor_version: u32,
659    pub config: BTreeMap<String, ConfigValue>,
660}
661
662#[derive(Clone, Debug, PartialEq, Eq, PartialOrd, Ord, Serialize, Deserialize)]
663pub struct PortRef {
664    pub node: NodeId,
665    pub port: String,
666}
667
668#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize)]
669pub struct Edge {
670    pub from: PortRef,
671    pub to: PortRef,
672}
673
674#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize)]
675pub struct Graph {
676    pub version: u32,
677    pub nodes: Vec<NodeInstance>,
678    pub edges: Vec<Edge>,
679    /// Cross-invocation edges are explicit and never participate in same-call
680    /// topological ordering.
681    #[serde(default)]
682    pub feedback: Vec<FeedbackEdge>,
683    /// External values accepted by this graph invocation. Every entry names a
684    /// concrete descriptor input port; declarations are canonicalized by the
685    /// compiler and may not overlap an edge binding.
686    #[serde(default)]
687    pub invocation_inputs: Vec<PortRef>,
688    pub required_capabilities: Vec<Capability>,
689    pub required_proofs: Vec<String>,
690    pub policy: Vec<String>,
691    pub minimum_fidelity: u16,
692    pub session: Option<SessionContract>,
693}
694
695#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize)]
696pub struct KernelDescriptor {
697    pub id: KernelId,
698    /// Exact semantic chain implemented by this kernel. A chain longer than
699    /// one is an explicit fused implementation, never an optimizer guess.
700    pub implements: Vec<NodeTypeRef>,
701    /// Content identity of implementation code/build inputs. It must change
702    /// whenever executable behavior changes, even if the local KernelId does not.
703    pub implementation_id: ImplementationId,
704    /// A physical layout conversion implemented by this kernel. Conversion
705    /// kernels have an empty `implements` chain and execute as explicit plan
706    /// steps with no semantic-node identity.
707    pub conversion: Option<LayoutConversion>,
708    pub target: Target,
709    pub input_layouts: Vec<Layout>,
710    pub output_layouts: Vec<Layout>,
711    pub resources: ResourceEnvelope,
712    pub determinism: Determinism,
713    pub lowering: String,
714}
715
716#[derive(Clone, Debug, PartialEq, Eq, PartialOrd, Ord, Serialize, Deserialize)]
717pub struct LayoutConversion {
718    pub semantic_type: String,
719    pub from: Layout,
720    pub to: Layout,
721    pub max_input_bytes: u64,
722    pub max_output_bytes: u64,
723}
724
725#[derive(Clone, Copy, Debug, PartialEq, Eq, Serialize, Deserialize)]
726#[serde(rename_all = "kebab-case")]
727pub enum OutputBinding {
728    Buffer(BufferId),
729    Terminal,
730}
731
732#[derive(Clone, Copy, Debug, PartialEq, Eq, Serialize, Deserialize)]
733#[serde(rename_all = "kebab-case")]
734pub enum InputBinding {
735    Buffer(BufferId),
736    Invocation(u32),
737    Feedback(FeedbackId),
738    Absent,
739}
740
741#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize)]
742pub struct CompiledPortContract {
743    pub name: String,
744    pub semantic_type: String,
745    pub optional: bool,
746    pub layout: Layout,
747    pub max_bytes: u64,
748    pub domain: DomainType,
749    pub proof: ProofContract,
750    pub policy: PolicyContract,
751    pub fidelity: FidelityContract,
752    pub extent: ExtentContract,
753    pub lease: LeaseContract,
754}
755
756impl CompiledPortContract {
757    pub fn opaque(
758        name: impl Into<String>,
759        semantic_type: impl Into<String>,
760        layout: Layout,
761        max_bytes: u64,
762    ) -> Self {
763        let port = PortDescriptor::opaque(name, semantic_type, max_bytes);
764        Self {
765            name: port.name,
766            semantic_type: port.semantic_type,
767            optional: port.optional,
768            layout,
769            max_bytes: port.max_bytes,
770            domain: port.domain,
771            proof: port.proof,
772            policy: port.policy,
773            fidelity: port.fidelity,
774            extent: port.extent,
775            lease: port.lease,
776        }
777    }
778}
779
780#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize)]
781pub struct CompiledNode {
782    /// Dense physical execution identity. Unlike `NodeId`, this includes
783    /// compiler-inserted conversion steps.
784    pub id: StepId,
785    /// One or more semantic nodes implemented by this step. This is empty only
786    /// for an explicit compiler-inserted physical conversion.
787    pub semantic_nodes: Vec<NodeId>,
788    /// Exact registered semantic types aligned one-to-one with
789    /// `semantic_nodes`; empty for conversion steps.
790    pub semantic_types: Vec<NodeTypeRef>,
791    /// Normalized instance configurations aligned one-to-one with
792    /// `semantic_nodes`; empty for conversion steps.
793    pub semantic_configs: Vec<BTreeMap<String, ConfigValue>>,
794    pub kernel: KernelId,
795    pub implementation_id: ImplementationId,
796    pub resources: ResourceEnvelope,
797    pub determinism: Determinism,
798    pub lowering: String,
799    pub conversion: Option<LayoutConversion>,
800    /// Stable physical port names aligned with the ordered bindings below.
801    pub input_ports: Vec<String>,
802    pub output_ports: Vec<String>,
803    pub input_contracts: Vec<CompiledPortContract>,
804    pub output_contracts: Vec<CompiledPortContract>,
805    /// One binding per physical kernel input, in descriptor port order.
806    pub input_bindings: Vec<InputBinding>,
807    /// One binding per physical kernel output, in descriptor port order.
808    /// Unconnected semantic outputs are explicit terminal invocation results.
809    pub output_bindings: Vec<OutputBinding>,
810    pub partiality: Partiality,
811    pub failure: FailureContract,
812    pub effect: Effect,
813    pub retry_limit: u16,
814    pub state: StateContract,
815    /// Identity lineage of semantic decompositions used to reach this step.
816    pub subgraph_path: Vec<SubgraphId>,
817}
818
819#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize)]
820pub struct FeedbackPlan {
821    pub id: FeedbackId,
822    pub from_step: StepId,
823    pub from_port: u32,
824    pub to_step: StepId,
825    pub to_port: u32,
826    pub delay: DelayContract,
827    pub state_bytes: u64,
828}
829
830#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize)]
831pub struct BufferPlan {
832    pub id: BufferId,
833    pub layout: Layout,
834    pub capacity_bytes: u64,
835    pub producer: StepId,
836    pub consumers: Vec<StepId>,
837    /// Cached final consumer in topological order for constant-time liveness release.
838    pub last_consumer: StepId,
839    pub aliases: Option<BufferId>,
840}
841
842#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize)]
843pub struct CompiledPlan {
844    pub schema_version: u32,
845    pub graph_id: GraphId,
846    pub plan_id: PlanId,
847    pub realm: ExecutionRealm,
848    pub order: Vec<NodeId>,
849    pub nodes: Vec<CompiledNode>,
850    pub buffers: Vec<BufferPlan>,
851    pub feedback: Vec<FeedbackPlan>,
852    /// Canonical port table addressed by `InputBinding::Invocation`.
853    pub invocation_ports: Vec<PortRef>,
854    pub propagated_proofs: Vec<String>,
855    pub propagated_policy: Vec<String>,
856    pub resulting_fidelity: u16,
857    /// Peak invocation memory: live buffers, kernel workspaces/scratch, and
858    /// invocation-scoped state. Session/durable state and feedback history are
859    /// excluded and accounted by `persistent_state_bytes`.
860    pub peak_bytes: u64,
861    /// Session/durable state plus feedback history retained across invocations.
862    pub persistent_state_bytes: u64,
863    pub session: Option<SessionContract>,
864}
865
866/// A compiled plan whose physical steps have been selected from, or checked
867/// against, a trusted kernel registry. Structural AOT decoding deliberately
868/// returns `CompiledPlan`; only local compilation or registry-bound decoding
869/// can construct this executable wrapper.
870#[derive(Clone, Debug, PartialEq, Eq)]
871pub struct AuthorizedPlan {
872    plan: CompiledPlan,
873}
874
875impl AuthorizedPlan {
876    pub(crate) const fn new(plan: CompiledPlan) -> Self {
877        Self { plan }
878    }
879
880    pub const fn as_plan(&self) -> &CompiledPlan {
881        &self.plan
882    }
883
884    pub fn into_plan(self) -> CompiledPlan {
885        self.plan
886    }
887}
888
889impl core::ops::Deref for AuthorizedPlan {
890    type Target = CompiledPlan;
891
892    fn deref(&self) -> &Self::Target {
893        &self.plan
894    }
895}