use std::collections::HashSet;
use bb_ir::proto::onnx::ModelProto;
use bb_ir::registry::find_concrete_component;
use crate::artifact::BindingSpec;
use crate::error::{CompileError, SlotSource};
fn canonical_role(required_trait: &str) -> &str {
required_trait
.strip_suffix("Runtime")
.unwrap_or(required_trait)
}
const REQUIRED_TRAIT_KEY: &str = "ai.bytesandbrains.required_trait";
pub(crate) fn validate_all_slots_bound(
spec: &BindingSpec,
models: &[ModelProto],
) -> Result<(), CompileError> {
validate_direct_placeholders(spec, models)?;
validate_dependency_slots(spec)?;
Ok(())
}
fn validate_direct_placeholders(
spec: &BindingSpec,
models: &[ModelProto],
) -> Result<(), CompileError> {
let bound_roles: HashSet<String> = spec
.slots
.iter()
.filter(|s| !s.concrete_type_name.is_empty())
.map(|s| canonical_role(&s.role).to_string())
.collect();
let mut required_roles: Vec<String> = Vec::new();
for model in models {
if let Some(graph) = &model.graph {
for node in &graph.node {
if let Some(role) = required_trait_of_node(node) {
let canon = canonical_role(role).to_string();
if !required_roles.contains(&canon) {
required_roles.push(canon);
}
}
}
}
for function in &model.functions {
for node in &function.node {
if let Some(role) = required_trait_of_node(node) {
let canon = canonical_role(role).to_string();
if !required_roles.contains(&canon) {
required_roles.push(canon);
}
}
}
}
}
for role in required_roles {
if !bound_roles.contains(&role) {
return Err(CompileError::UnboundSlot {
role,
source: SlotSource::DirectPlaceholder,
});
}
}
Ok(())
}
fn validate_dependency_slots(spec: &BindingSpec) -> Result<(), CompileError> {
for slot in &spec.slots {
if slot.concrete_type_name.is_empty() {
continue;
}
let Some(entry) = find_concrete_component(&slot.concrete_type_name) else {
continue;
};
for dep in entry.dependencies {
let Some(target) = spec.get(dep.slot) else {
return Err(CompileError::UnboundSlot {
role: dep.role.to_string(),
source: SlotSource::DependencyOf {
component: slot.concrete_type_name.clone(),
bound_at_slot: slot.slot.clone(),
required_slot: dep.slot.to_string(),
},
});
};
if target.concrete_type_name.is_empty() {
return Err(CompileError::UnboundSlot {
role: dep.role.to_string(),
source: SlotSource::DependencyOf {
component: slot.concrete_type_name.clone(),
bound_at_slot: slot.slot.clone(),
required_slot: dep.slot.to_string(),
},
});
}
}
}
Ok(())
}
fn required_trait_of_node(node: &bb_ir::proto::onnx::NodeProto) -> Option<&str> {
node.metadata_props
.iter()
.find(|p| p.key == REQUIRED_TRAIT_KEY)
.map(|p| p.value.as_str())
}