1use crate::manifest::ImplKind;
7use serde::{Deserialize, Serialize};
8
9#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize, Default)]
12#[serde(rename_all = "snake_case")]
13pub enum Hardware {
14 #[default]
15 Cpu,
16 Gpu,
17}
18
19#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
21pub struct CapabilityEnvelope {
22 #[serde(default)]
23 pub hardware: Hardware,
24 pub deterministic: bool,
26 pub io: bool,
28 pub streaming: bool,
30}
31
32impl Default for CapabilityEnvelope {
33 fn default() -> Self {
35 Self {
36 hardware: Hardware::Cpu,
37 deterministic: true,
38 io: false,
39 streaming: false,
40 }
41 }
42}
43
44#[derive(Debug, Clone, Copy, PartialEq, Eq)]
46pub enum Binding {
47 Stream,
51 Derived,
54 Action,
58}
59
60#[derive(Debug, Clone, PartialEq, Eq, thiserror::Error)]
63pub enum EnvelopeViolation {
64 #[error("binding {binding:?} forbids I/O (io=true); use an Action binding for effectful computes")]
65 IoNotAllowed { binding: Binding },
66 #[error("binding {binding:?} requires deterministic=true (a derived/stream edge must be cacheable)")]
67 NondeterministicNotAllowed { binding: Binding },
68 #[error("binding {binding:?} forbids impl=llm (nondeterministic/effectful); use an Action binding")]
69 LlmNotAllowed { binding: Binding },
70}
71
72impl Binding {
73 fn is_strict(self) -> bool {
75 matches!(self, Binding::Stream | Binding::Derived)
76 }
77
78 pub fn accepts(self, env: &CapabilityEnvelope, impl_kind: ImplKind) -> Result<(), EnvelopeViolation> {
82 if self.is_strict() {
83 if env.io {
84 return Err(EnvelopeViolation::IoNotAllowed { binding: self });
85 }
86 if !env.deterministic {
87 return Err(EnvelopeViolation::NondeterministicNotAllowed { binding: self });
88 }
89 if impl_kind == ImplKind::Llm {
90 return Err(EnvelopeViolation::LlmNotAllowed { binding: self });
91 }
92 }
93 Ok(())
94 }
95}
96
97#[cfg(test)]
98mod tests {
99 use super::*;
100
101 fn pure() -> CapabilityEnvelope {
102 CapabilityEnvelope::default()
103 }
104
105 #[test]
106 fn default_envelope_is_pure_cpu_batch() {
107 let e = pure();
108 assert_eq!(e.hardware, Hardware::Cpu);
109 assert!(e.deterministic && !e.io && !e.streaming);
110 }
111
112 #[test]
113 fn stream_binding_accepts_pure_compute() {
114 assert!(Binding::Stream.accepts(&pure(), ImplKind::Wasm).is_ok());
115 assert!(Binding::Derived.accepts(&pure(), ImplKind::Expression).is_ok());
116 assert!(Binding::Derived.accepts(&pure(), ImplKind::Builtin).is_ok());
117 }
118
119 #[test]
120 fn stream_binding_rejects_io_nondeterminism_and_llm() {
121 let io = CapabilityEnvelope { io: true, ..pure() };
122 assert_eq!(
123 Binding::Stream.accepts(&io, ImplKind::Wasm),
124 Err(EnvelopeViolation::IoNotAllowed {
125 binding: Binding::Stream
126 })
127 );
128 let nd = CapabilityEnvelope {
129 deterministic: false,
130 ..pure()
131 };
132 assert_eq!(
133 Binding::Derived.accepts(&nd, ImplKind::Container),
134 Err(EnvelopeViolation::NondeterministicNotAllowed {
135 binding: Binding::Derived
136 })
137 );
138 assert_eq!(
139 Binding::Stream.accepts(&pure(), ImplKind::Llm),
140 Err(EnvelopeViolation::LlmNotAllowed {
141 binding: Binding::Stream
142 })
143 );
144 }
145
146 #[test]
147 fn action_binding_permits_everything() {
148 let effectful = CapabilityEnvelope {
149 io: true,
150 deterministic: false,
151 ..pure()
152 };
153 assert!(Binding::Action.accepts(&effectful, ImplKind::Llm).is_ok());
154 assert!(Binding::Action.accepts(&effectful, ImplKind::Container).is_ok());
155 }
156}