1use serde::{Deserialize, Serialize};
4
5use crate::ast::{ActionNode, ParamExpr};
6use crate::normalizer::{NormalizedParam, NormalizedSequence};
7
8#[derive(Debug, Clone, Serialize, Deserialize)]
10pub struct CompiledAction {
11 pub id: String,
12 pub signature: String,
13 pub ast: ActionNode,
14 pub required_variables: Vec<String>,
15 pub compiled_at: String,
16 pub source_occurrences: u32,
17 pub source_success_rate: f64,
18}
19
20pub struct ActionCompiler;
22
23impl ActionCompiler {
24 pub fn compile(
26 normalized: &NormalizedSequence,
27 occurrences: u32,
28 success_rate: f64,
29 ) -> CompiledAction {
30 let required_variables: Vec<String> = normalized.variables.keys().cloned().collect();
31
32 let ast = if normalized.actions.len() == 1 {
33 Self::compile_single_action(&normalized.actions[0])
34 } else {
35 Self::compile_sequence(normalized)
36 };
37
38 CompiledAction {
39 id: uuid::Uuid::new_v4().to_string(),
40 signature: normalized.signature.clone(),
41 ast,
42 required_variables,
43 compiled_at: chrono::Utc::now().to_rfc3339(),
44 source_occurrences: occurrences,
45 source_success_rate: success_rate,
46 }
47 }
48
49 fn compile_single_action(action: &crate::normalizer::NormalizedAction) -> ActionNode {
50 ActionNode::Action {
51 tool: action.tool.clone(),
52 params: action
53 .params
54 .iter()
55 .map(|(k, v)| (k.clone(), Self::param_to_expr(v)))
56 .collect(),
57 }
58 }
59
60 fn compile_sequence(normalized: &NormalizedSequence) -> ActionNode {
61 let nodes: Vec<ActionNode> = normalized
62 .actions
63 .iter()
64 .map(|a| Self::compile_single_action(a))
65 .collect();
66
67 ActionNode::Sequence(nodes)
68 }
69
70 fn param_to_expr(param: &NormalizedParam) -> ParamExpr {
71 match param {
72 NormalizedParam::Literal(v) => ParamExpr::Literal(v.clone()),
73 NormalizedParam::Variable { name } => ParamExpr::Variable(name.clone()),
74 }
75 }
76}
77
78#[cfg(test)]
79mod tests {
80 use super::*;
81 use crate::normalizer::{
82 InferredType, NormalizedAction, NormalizedParam, NormalizedSequence, VariableInfo,
83 };
84 use std::collections::HashMap;
85
86 fn simple_sequence() -> NormalizedSequence {
87 NormalizedSequence {
88 actions: vec![
89 NormalizedAction {
90 tool: "git_add".into(),
91 params: HashMap::from([(
92 "path".into(),
93 NormalizedParam::Literal(serde_json::json!(".")),
94 )]),
95 },
96 NormalizedAction {
97 tool: "git_commit".into(),
98 params: HashMap::from([(
99 "message".into(),
100 NormalizedParam::Variable {
101 name: "var_0".into(),
102 },
103 )]),
104 },
105 ],
106 variables: HashMap::from([(
107 "var_0".into(),
108 VariableInfo {
109 name: "var_0".into(),
110 sample_values: vec![serde_json::json!("fix: bug")],
111 inferred_type: InferredType::String,
112 },
113 )]),
114 signature: "git_add→git_commit".into(),
115 }
116 }
117
118 #[test]
119 fn test_compile_simple() {
120 let norm = simple_sequence();
121 let compiled = ActionCompiler::compile(&norm, 5, 1.0);
122 assert_eq!(compiled.signature, "git_add→git_commit");
123 assert_eq!(compiled.required_variables, vec!["var_0"]);
124 assert_eq!(compiled.source_occurrences, 5);
125 assert_eq!(compiled.ast.action_count(), 2);
126 }
127
128 #[test]
129 fn test_compile_single_action() {
130 let norm = NormalizedSequence {
131 actions: vec![NormalizedAction {
132 tool: "deploy".into(),
133 params: HashMap::from([(
134 "env".into(),
135 NormalizedParam::Literal(serde_json::json!("prod")),
136 )]),
137 }],
138 variables: HashMap::new(),
139 signature: "deploy".into(),
140 };
141 let compiled = ActionCompiler::compile(&norm, 3, 1.0);
142 assert_eq!(compiled.ast.action_count(), 1);
143 assert!(compiled.required_variables.is_empty());
144 }
145
146 #[test]
147 fn test_compiled_serializable() {
148 let norm = simple_sequence();
149 let compiled = ActionCompiler::compile(&norm, 5, 1.0);
150 let json = serde_json::to_string(&compiled).unwrap();
151 assert!(json.contains("git_add"));
152 assert!(json.contains("git_commit"));
153 }
154
155 #[test]
156 fn test_compiled_action_has_id() {
157 let norm = simple_sequence();
158 let compiled = ActionCompiler::compile(&norm, 1, 0.9);
159 assert!(!compiled.id.is_empty());
160 }
161
162 #[test]
163 fn test_compiled_action_has_timestamp() {
164 let norm = simple_sequence();
165 let compiled = ActionCompiler::compile(&norm, 1, 1.0);
166 assert!(!compiled.compiled_at.is_empty());
167 }
168
169 #[test]
170 fn test_compiled_preserves_success_rate() {
171 let norm = simple_sequence();
172 let compiled = ActionCompiler::compile(&norm, 10, 0.85);
173 assert_eq!(compiled.source_success_rate, 0.85);
174 assert_eq!(compiled.source_occurrences, 10);
175 }
176
177 #[test]
178 fn test_compiled_action_serde_roundtrip() {
179 let norm = simple_sequence();
180 let compiled = ActionCompiler::compile(&norm, 3, 1.0);
181 let json = serde_json::to_string(&compiled).unwrap();
182 let restored: CompiledAction = serde_json::from_str(&json).unwrap();
183 assert_eq!(restored.signature, "git_add→git_commit");
184 assert_eq!(restored.ast.action_count(), 2);
185 }
186
187 #[test]
188 fn test_compile_no_variables() {
189 let norm = NormalizedSequence {
190 actions: vec![NormalizedAction {
191 tool: "test".into(),
192 params: HashMap::from([("key".into(), NormalizedParam::Literal(serde_json::json!("val")))]),
193 }],
194 variables: HashMap::new(),
195 signature: "test".into(),
196 };
197 let compiled = ActionCompiler::compile(&norm, 1, 1.0);
198 assert!(compiled.required_variables.is_empty());
199 }
200}