1use 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#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
82pub struct PartitionConfig {
83 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}