use crate::application::cli::error::CliError;
use serde::{Deserialize, Serialize};
#[derive(Debug, Clone, Serialize, Deserialize)]
#[serde(tag = "type")]
pub enum BattalionYamlConfig {
#[serde(rename = "formation")]
Formation(FormationConfig),
#[serde(rename = "phalanx")]
Phalanx(PhalanxConfig),
#[serde(rename = "campaign")]
Campaign(CampaignConfig),
#[serde(rename = "chain-of-command")]
ChainOfCommand(ChainOfCommandConfig),
#[serde(rename = "conclave")]
Conclave(ConclaveConfig),
#[serde(rename = "maneuver")]
Maneuver(ManeuverConfig),
}
impl BattalionYamlConfig {
pub fn battalion_type(&self) -> &str {
match self {
BattalionYamlConfig::Formation(_) => "formation",
BattalionYamlConfig::Phalanx(_) => "phalanx",
BattalionYamlConfig::Campaign(_) => "campaign",
BattalionYamlConfig::ChainOfCommand(_) => "chain-of-command",
BattalionYamlConfig::Conclave(_) => "conclave",
BattalionYamlConfig::Maneuver(_) => "maneuver",
}
}
pub fn validate(&self) -> Result<(), CliError> {
match self {
BattalionYamlConfig::Formation(config) => config.validate(),
BattalionYamlConfig::Phalanx(config) => config.validate(),
BattalionYamlConfig::Campaign(config) => config.validate(),
BattalionYamlConfig::ChainOfCommand(config) => config.validate(),
BattalionYamlConfig::Conclave(config) => config.validate(),
BattalionYamlConfig::Maneuver(config) => config.validate(),
}
}
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct FormationConfig {
pub name: String,
pub paladins: Vec<PaladinReference>,
#[serde(default = "default_true")]
pub pass_output_to_next: bool,
}
impl FormationConfig {
fn validate(&self) -> Result<(), CliError> {
if self.name.is_empty() {
return Err(CliError::MissingRequiredField {
field: "name".to_string(),
message: "Campaign name is required".to_string(),
});
}
if self.paladins.is_empty() {
return Err(CliError::ValidationError {
message: "Formation must have at least one Paladin".to_string(),
});
}
Ok(())
}
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct PhalanxConfig {
pub name: String,
pub paladins: Vec<PaladinReference>,
#[serde(skip_serializing_if = "Option::is_none")]
pub inputs: Option<Vec<String>>,
}
impl PhalanxConfig {
fn validate(&self) -> Result<(), CliError> {
if self.name.is_empty() {
return Err(CliError::MissingRequiredField {
field: "name".to_string(),
message: "Phalanx name is required".to_string(),
});
}
if self.paladins.is_empty() {
return Err(CliError::ValidationError {
message: "Phalanx must have at least one Paladin".to_string(),
});
}
if let Some(inputs) = &self.inputs
&& inputs.len() != self.paladins.len()
{
return Err(CliError::ValidationError {
message: format!(
"Number of inputs ({}) must match number of Paladins ({})",
inputs.len(),
self.paladins.len()
),
});
}
Ok(())
}
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct CampaignConfig {
pub name: String,
pub nodes: Vec<CampaignNode>,
pub edges: Vec<CampaignEdge>,
pub start_node: String,
}
impl CampaignConfig {
fn validate(&self) -> Result<(), CliError> {
if self.name.is_empty() {
return Err(CliError::MissingRequiredField {
field: "name".to_string(),
message: "Campaign name is required".to_string(),
});
}
if self.nodes.is_empty() {
return Err(CliError::ValidationError {
message: "Campaign must have at least one node".to_string(),
});
}
if !self.nodes.iter().any(|n| n.id == self.start_node) {
return Err(CliError::ValidationError {
message: format!("Start node '{}' not found in nodes", self.start_node),
});
}
for edge in &self.edges {
if !self.nodes.iter().any(|n| n.id == edge.from) {
return Err(CliError::ValidationError {
message: format!("Edge references non-existent node: {}", edge.from),
});
}
if !self.nodes.iter().any(|n| n.id == edge.to) {
return Err(CliError::ValidationError {
message: format!("Edge references non-existent node: {}", edge.to),
});
}
}
Ok(())
}
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct CampaignNode {
pub id: String,
pub paladin: PaladinReference,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct CampaignEdge {
pub from: String,
pub to: String,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct ChainOfCommandConfig {
pub name: String,
pub commander: PaladinReference,
pub delegates: Vec<PaladinReference>,
}
impl ChainOfCommandConfig {
fn validate(&self) -> Result<(), CliError> {
if self.name.is_empty() {
return Err(CliError::MissingRequiredField {
field: "name".to_string(),
message: "Chain of Command name is required".to_string(),
});
}
if self.delegates.is_empty() {
return Err(CliError::ValidationError {
message: "Chain of Command must have at least one delegate".to_string(),
});
}
Ok(())
}
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct ConclaveConfig {
pub name: String,
pub experts: Vec<PaladinReference>,
pub aggregator: PaladinReference,
#[serde(skip_serializing_if = "Option::is_none")]
pub timeout_seconds: Option<u64>,
#[serde(skip_serializing_if = "Option::is_none")]
pub retry_attempts: Option<u32>,
#[serde(skip_serializing_if = "Option::is_none")]
pub synthesis_prompt: Option<String>,
#[serde(skip_serializing_if = "Option::is_none")]
pub include_expert_names: Option<bool>,
#[serde(skip_serializing_if = "Option::is_none")]
pub max_expert_output_tokens: Option<usize>,
#[serde(skip_serializing_if = "Option::is_none")]
pub observability_level: Option<String>,
}
impl ConclaveConfig {
fn validate(&self) -> Result<(), CliError> {
if self.name.is_empty() {
return Err(CliError::MissingRequiredField {
field: "name".to_string(),
message: "Conclave name is required".to_string(),
});
}
if self.experts.len() < 2 {
return Err(CliError::ValidationError {
message: "Conclave requires at least 2 expert Paladins".to_string(),
});
}
if let Some(timeout) = self.timeout_seconds
&& timeout == 0
{
return Err(CliError::ValidationError {
message: "Timeout must be greater than 0".to_string(),
});
}
if let Some(ref level) = self.observability_level {
let valid_levels = ["minimal", "standard", "verbose"];
if !valid_levels.contains(&level.as_str()) {
return Err(CliError::InvalidFieldValue {
field: "observability_level".to_string(),
message: format!(
"must be one of: {}. Got: {}",
valid_levels.join(", "),
level
),
});
}
}
Ok(())
}
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct ManeuverConfig {
pub name: String,
pub flow: String,
pub paladins: Vec<PaladinReference>,
#[serde(skip_serializing_if = "Option::is_none")]
pub visualize: Option<String>,
}
impl ManeuverConfig {
fn validate(&self) -> Result<(), CliError> {
if self.name.is_empty() {
return Err(CliError::MissingRequiredField {
field: "name".to_string(),
message: "Maneuver name is required".to_string(),
});
}
if self.flow.is_empty() {
return Err(CliError::MissingRequiredField {
field: "flow".to_string(),
message: "Flow expression is required".to_string(),
});
}
if self.paladins.is_empty() {
return Err(CliError::ValidationError {
message: "Maneuver must have at least one Paladin".to_string(),
});
}
use crate::core::platform::container::battalion::parser::FlowParser;
FlowParser::parse(&self.flow).map_err(|e| CliError::ValidationError {
message: format!("Invalid flow expression: {}", e),
})?;
Ok(())
}
}
#[derive(Debug, Clone, Serialize, Deserialize)]
#[serde(untagged)]
pub enum PaladinReference {
File { file: String },
Inline(Box<crate::application::cli::config::paladin_config::PaladinYamlConfig>),
}
fn default_true() -> bool {
true
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_formation_valid() {
let config = FormationConfig {
name: "test".to_string(),
paladins: vec![PaladinReference::File {
file: "paladin1.yaml".to_string(),
}],
pass_output_to_next: true,
};
assert!(config.validate().is_ok());
}
#[test]
fn test_formation_empty_name() {
let config = FormationConfig {
name: "".to_string(),
paladins: vec![PaladinReference::File {
file: "paladin1.yaml".to_string(),
}],
pass_output_to_next: true,
};
assert!(matches!(
config.validate(),
Err(CliError::MissingRequiredField { .. })
));
}
#[test]
fn test_phalanx_inputs_mismatch() {
let config = PhalanxConfig {
name: "test".to_string(),
paladins: vec![
PaladinReference::File {
file: "paladin1.yaml".to_string(),
},
PaladinReference::File {
file: "paladin2.yaml".to_string(),
},
],
inputs: Some(vec!["input1".to_string()]), };
assert!(matches!(
config.validate(),
Err(CliError::ValidationError { .. })
));
}
}