Skip to main content

mur_common/
parallel.rs

1//! Parallel tracks config — extends fleet.yaml `parallel:` section.
2
3use serde::{Deserialize, Serialize};
4
5#[derive(Debug, Clone, Default, PartialEq, Serialize, Deserialize)]
6#[serde(rename_all = "snake_case")]
7pub enum ParallelMode {
8    #[default]
9    Speculative,
10    Partition,
11}
12
13#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
14pub struct TrackConfig {
15    pub name: String,
16    pub approach: String,
17    #[serde(default, skip_serializing_if = "Option::is_none")]
18    pub model: Option<String>,
19}
20
21#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
22pub struct Rubric {
23    #[serde(default = "default_correctness")]
24    pub correctness: f32,
25    #[serde(default = "default_design")]
26    pub design: f32,
27    #[serde(default = "default_maintainability")]
28    pub maintainability: f32,
29    #[serde(default = "default_security")]
30    pub security: f32,
31}
32
33fn default_correctness() -> f32 {
34    0.40
35}
36fn default_design() -> f32 {
37    0.30
38}
39fn default_maintainability() -> f32 {
40    0.20
41}
42fn default_security() -> f32 {
43    0.10
44}
45
46impl Default for Rubric {
47    fn default() -> Self {
48        Self {
49            correctness: default_correctness(),
50            design: default_design(),
51            maintainability: default_maintainability(),
52            security: default_security(),
53        }
54    }
55}
56
57impl Rubric {
58    pub fn version(&self) -> String {
59        format!(
60            "c{:.2}d{:.2}m{:.2}s{:.2}",
61            self.correctness, self.design, self.maintainability, self.security
62        )
63    }
64}
65
66#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
67pub struct JudgeConfig {
68    pub model: String,
69    #[serde(default)]
70    pub rubric: Rubric,
71}
72
73#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
74#[serde(rename_all = "snake_case")]
75pub enum PreFilterKind {
76    CargoCheck,
77    CargoClippyDeny,
78}
79
80/// Config for `ParallelMode::Partition`: split one file into disjoint regions.
81#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
82pub struct PartitionConfig {
83    /// Repo-relative path of the single file to partition.
84    pub target_file: String,
85}
86
87#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
88pub struct ParallelConfig {
89    #[serde(default)]
90    pub mode: ParallelMode,
91    pub tracks: Vec<TrackConfig>,
92    pub judge: JudgeConfig,
93    #[serde(default, skip_serializing_if = "Vec::is_empty")]
94    pub pre_filter: Vec<PreFilterKind>,
95    #[serde(default, skip_serializing_if = "Option::is_none")]
96    pub partition: Option<PartitionConfig>,
97}
98
99#[cfg(test)]
100mod tests {
101    use super::*;
102
103    #[test]
104    fn rubric_version_is_stable() {
105        let r = Rubric::default();
106        assert_eq!(r.version(), "c0.40d0.30m0.20s0.10");
107    }
108
109    #[test]
110    fn parallel_config_roundtrips_yaml() {
111        let yaml = r#"
112mode: speculative
113judge:
114  model: claude-opus-4-8
115tracks:
116  - name: track-a
117    approach: "functional style"
118  - name: track-b
119    approach: "performance first"
120"#;
121        let cfg: ParallelConfig = serde_yaml::from_str(yaml).unwrap();
122        assert_eq!(cfg.tracks.len(), 2);
123        assert_eq!(cfg.mode, ParallelMode::Speculative);
124        let back = serde_yaml::to_string(&cfg).unwrap();
125        let cfg2: ParallelConfig = serde_yaml::from_str(&back).unwrap();
126        assert_eq!(cfg, cfg2);
127    }
128
129    #[test]
130    fn partition_config_roundtrips() {
131        let yaml = r#"
132mode: partition
133judge:
134  model: claude-opus-4-8
135tracks: []
136partition:
137  target_file: src/widget.rs
138"#;
139        let cfg: ParallelConfig = serde_yaml::from_str(yaml).unwrap();
140        assert_eq!(cfg.mode, ParallelMode::Partition);
141        let p = cfg.partition.as_ref().unwrap();
142        assert_eq!(p.target_file, "src/widget.rs");
143        let back = serde_yaml::to_string(&cfg).unwrap();
144        let cfg2: ParallelConfig = serde_yaml::from_str(&back).unwrap();
145        assert_eq!(cfg, cfg2);
146    }
147
148    #[test]
149    fn partition_absent_by_default() {
150        let yaml = r#"
151judge:
152  model: claude-opus-4-8
153tracks: []
154"#;
155        let cfg: ParallelConfig = serde_yaml::from_str(yaml).unwrap();
156        assert!(cfg.partition.is_none());
157    }
158}