Skip to main content

orchestrator_config/config/
item_select.rs

1use serde::{Deserialize, Serialize};
2use std::collections::HashMap;
3
4/// WP03: Configuration for the item_select builtin step.
5#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
6pub struct ItemSelectConfig {
7    /// Selection strategy.
8    pub strategy: SelectionStrategy,
9    /// Pipeline variable name containing the metric to evaluate (for min/max/threshold).
10    #[serde(default, skip_serializing_if = "Option::is_none")]
11    pub metric_var: Option<String>,
12    /// Weight map for weighted strategy: var_name → weight.
13    #[serde(default, skip_serializing_if = "Option::is_none")]
14    pub weights: Option<HashMap<String, f64>>,
15    /// Threshold value (for threshold strategy).
16    #[serde(default, skip_serializing_if = "Option::is_none")]
17    pub threshold: Option<f64>,
18    /// Where to persist the selection result in the workflow store.
19    #[serde(default, skip_serializing_if = "Option::is_none")]
20    pub store_result: Option<StoreTarget>,
21    /// How to break ties (default: first).
22    #[serde(default)]
23    pub tie_break: TieBreak,
24}
25
26/// Selection strategy for item_select.
27#[derive(Debug, Clone, Copy, Serialize, Deserialize, PartialEq, Eq)]
28#[serde(rename_all = "snake_case")]
29pub enum SelectionStrategy {
30    /// Keep the item with the lowest metric value.
31    Min,
32    /// Keep the item with the highest metric value.
33    Max,
34    /// Keep items that satisfy the configured threshold rule.
35    Threshold,
36    /// Score items with weighted variables and keep the highest-ranked one.
37    Weighted,
38}
39
40/// Where to store the selection result.
41#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
42pub struct StoreTarget {
43    /// Workflow-store namespace or store identifier.
44    pub namespace: String,
45    /// Key written inside the target store.
46    pub key: String,
47}
48
49/// Tie-breaking strategy.
50#[derive(Debug, Clone, Copy, Serialize, Deserialize, PartialEq, Eq, Default)]
51#[serde(rename_all = "snake_case")]
52pub enum TieBreak {
53    /// Keep the first candidate in deterministic order.
54    #[default]
55    First,
56    /// Keep the last candidate in deterministic order.
57    Last,
58    /// Select one winner randomly.
59    Random,
60}
61
62/// Result of a selection operation.
63#[derive(Debug, Clone)]
64pub struct SelectionResult {
65    /// Identifier of the surviving item.
66    pub winner_id: String,
67    /// Identifiers of items removed from the candidate set.
68    pub eliminated_ids: Vec<String>,
69    /// Variable map attached to the winning item.
70    pub winner_vars: HashMap<String, String>,
71}
72
73#[cfg(test)]
74mod tests {
75    use super::*;
76
77    #[test]
78    fn test_item_select_config_min() {
79        let json = r#"{
80            "strategy": "min",
81            "metric_var": "error_count"
82        }"#;
83        let cfg: ItemSelectConfig =
84            serde_json::from_str(json).expect("deserialize item select config");
85        assert_eq!(cfg.strategy, SelectionStrategy::Min);
86        assert_eq!(cfg.metric_var, Some("error_count".to_string()));
87        assert_eq!(cfg.tie_break, TieBreak::First);
88    }
89
90    #[test]
91    fn test_item_select_config_weighted() {
92        let json = r#"{
93            "strategy": "weighted",
94            "weights": {"quality": 0.7, "speed": 0.3},
95            "tie_break": "random",
96            "store_result": {"namespace": "results", "key": "winner"}
97        }"#;
98        let cfg: ItemSelectConfig =
99            serde_json::from_str(json).expect("deserialize weighted config");
100        assert_eq!(cfg.strategy, SelectionStrategy::Weighted);
101        assert_eq!(cfg.tie_break, TieBreak::Random);
102        assert!(cfg.weights.is_some());
103        assert!(cfg.store_result.is_some());
104    }
105
106    #[test]
107    fn test_selection_strategy_serde() {
108        for s in &["\"min\"", "\"max\"", "\"threshold\"", "\"weighted\""] {
109            let strategy: SelectionStrategy =
110                serde_json::from_str(s).expect("deserialize strategy");
111            let json = serde_json::to_string(&strategy).expect("serialize strategy");
112            assert_eq!(&json, s);
113        }
114    }
115
116    #[test]
117    fn test_tie_break_default() {
118        let tb = TieBreak::default();
119        assert_eq!(tb, TieBreak::First);
120    }
121}