use crate::manifest::ImplKind;
use serde::{Deserialize, Serialize};
#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize, Default)]
#[serde(rename_all = "snake_case")]
pub enum Hardware {
#[default]
Cpu,
Gpu,
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
pub struct CapabilityEnvelope {
#[serde(default)]
pub hardware: Hardware,
pub deterministic: bool,
pub io: bool,
pub streaming: bool,
}
impl Default for CapabilityEnvelope {
fn default() -> Self {
Self {
hardware: Hardware::Cpu,
deterministic: true,
io: false,
streaming: false,
}
}
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum Binding {
Stream,
Derived,
Action,
}
#[derive(Debug, Clone, PartialEq, Eq, thiserror::Error)]
pub enum EnvelopeViolation {
#[error("binding {binding:?} forbids I/O (io=true); use an Action binding for effectful computes")]
IoNotAllowed { binding: Binding },
#[error("binding {binding:?} requires deterministic=true (a derived/stream edge must be cacheable)")]
NondeterministicNotAllowed { binding: Binding },
#[error("binding {binding:?} forbids impl=llm (nondeterministic/effectful); use an Action binding")]
LlmNotAllowed { binding: Binding },
}
impl Binding {
fn is_strict(self) -> bool {
matches!(self, Binding::Stream | Binding::Derived)
}
pub fn accepts(self, env: &CapabilityEnvelope, impl_kind: ImplKind) -> Result<(), EnvelopeViolation> {
if self.is_strict() {
if env.io {
return Err(EnvelopeViolation::IoNotAllowed { binding: self });
}
if !env.deterministic {
return Err(EnvelopeViolation::NondeterministicNotAllowed { binding: self });
}
if impl_kind == ImplKind::Llm {
return Err(EnvelopeViolation::LlmNotAllowed { binding: self });
}
}
Ok(())
}
}
#[cfg(test)]
mod tests {
use super::*;
fn pure() -> CapabilityEnvelope {
CapabilityEnvelope::default()
}
#[test]
fn default_envelope_is_pure_cpu_batch() {
let e = pure();
assert_eq!(e.hardware, Hardware::Cpu);
assert!(e.deterministic && !e.io && !e.streaming);
}
#[test]
fn stream_binding_accepts_pure_compute() {
assert!(Binding::Stream.accepts(&pure(), ImplKind::Wasm).is_ok());
assert!(Binding::Derived.accepts(&pure(), ImplKind::Expression).is_ok());
assert!(Binding::Derived.accepts(&pure(), ImplKind::Builtin).is_ok());
}
#[test]
fn stream_binding_rejects_io_nondeterminism_and_llm() {
let io = CapabilityEnvelope { io: true, ..pure() };
assert_eq!(
Binding::Stream.accepts(&io, ImplKind::Wasm),
Err(EnvelopeViolation::IoNotAllowed {
binding: Binding::Stream
})
);
let nd = CapabilityEnvelope {
deterministic: false,
..pure()
};
assert_eq!(
Binding::Derived.accepts(&nd, ImplKind::Container),
Err(EnvelopeViolation::NondeterministicNotAllowed {
binding: Binding::Derived
})
);
assert_eq!(
Binding::Stream.accepts(&pure(), ImplKind::Llm),
Err(EnvelopeViolation::LlmNotAllowed {
binding: Binding::Stream
})
);
}
#[test]
fn action_binding_permits_everything() {
let effectful = CapabilityEnvelope {
io: true,
deterministic: false,
..pure()
};
assert!(Binding::Action.accepts(&effectful, ImplKind::Llm).is_ok());
assert!(Binding::Action.accepts(&effectful, ImplKind::Container).is_ok());
}
}