orchestrator_config/config/
step_conventions.rs1use super::{CaptureDecl, CaptureSource, PostAction, StepScope};
10use serde::Deserialize;
11use std::collections::HashMap;
12use std::sync::LazyLock;
13
14pub static CONVENTIONS: LazyLock<StepConventionRegistry> =
16 LazyLock::new(StepConventionRegistry::builtin);
17
18#[derive(Debug, Clone, Default)]
22pub struct StepConvention {
23 pub builtin: Option<String>,
25 pub scope: Option<StepScope>,
27 pub is_guard: bool,
29 pub collect_artifacts: bool,
31 pub captures: Vec<CaptureDecl>,
33 pub post_actions: Vec<PostAction>,
35}
36
37#[derive(Debug, Default)]
39pub struct StepConventionRegistry {
40 conventions: HashMap<String, StepConvention>,
41}
42
43impl StepConventionRegistry {
44 fn builtin() -> Self {
46 let yaml = include_str!("sdlc_conventions.yaml");
47 let raw: RawConventions = match serde_yaml::from_str(yaml) {
48 Ok(v) => v,
49 Err(_) => return Self::default(),
51 };
52
53 let mut conventions = HashMap::new();
54 for (id, entry) in raw.steps {
55 let scope = entry.scope.as_deref().map(|s| match s {
56 "item" => StepScope::Item,
57 _ => StepScope::Task,
58 });
59
60 let captures = entry
61 .captures
62 .into_iter()
63 .filter_map(|c| {
64 let source = match c.source.as_str() {
65 "failed_flag" => CaptureSource::FailedFlag,
66 "success_flag" => CaptureSource::SuccessFlag,
67 "stdout" => CaptureSource::Stdout,
68 "stderr" => CaptureSource::Stderr,
69 "exit_code" => CaptureSource::ExitCode,
70 _ => return None,
71 };
72 Some(CaptureDecl {
73 var: c.var,
74 source,
75 json_path: None,
76 })
77 })
78 .collect();
79
80 let post_actions = entry
81 .post_actions
82 .into_iter()
83 .filter_map(|a| match a.as_str() {
84 "create_ticket" => Some(PostAction::CreateTicket),
85 "scan_tickets" => Some(PostAction::ScanTickets),
86 _ => None,
87 })
88 .collect();
89
90 conventions.insert(
91 id,
92 StepConvention {
93 builtin: entry.builtin,
94 scope,
95 is_guard: entry.is_guard,
96 collect_artifacts: entry.collect_artifacts,
97 captures,
98 post_actions,
99 },
100 );
101 }
102
103 Self { conventions }
104 }
105
106 pub fn lookup(&self, step_id: &str) -> Option<&StepConvention> {
108 self.conventions.get(step_id)
109 }
110
111 pub fn default_scope(&self, step_id: &str) -> StepScope {
114 self.conventions
115 .get(step_id)
116 .and_then(|c| c.scope)
117 .unwrap_or(StepScope::Task)
118 }
119
120 pub fn builtin_name(&self, step_id: &str) -> Option<String> {
122 self.conventions
123 .get(step_id)
124 .and_then(|c| c.builtin.clone())
125 }
126
127 pub fn is_known_builtin(&self, step_id: &str) -> bool {
129 self.conventions
130 .get(step_id)
131 .and_then(|c| c.builtin.as_ref())
132 .is_some()
133 }
134}
135
136#[derive(Deserialize)]
139struct RawConventions {
140 steps: HashMap<String, RawStepConvention>,
141}
142
143#[derive(Deserialize)]
144struct RawStepConvention {
145 #[serde(default)]
146 builtin: Option<String>,
147 #[serde(default)]
148 scope: Option<String>,
149 #[serde(default)]
150 is_guard: bool,
151 #[serde(default)]
152 collect_artifacts: bool,
153 #[serde(default)]
154 captures: Vec<RawCapture>,
155 #[serde(default)]
156 post_actions: Vec<String>,
157}
158
159#[derive(Deserialize)]
160struct RawCapture {
161 var: String,
162 source: String,
163}
164
165#[cfg(test)]
168mod tests {
169 use super::*;
170
171 #[test]
172 fn builtin_conventions_parse() {
173 let registry = StepConventionRegistry::builtin();
174 assert!(registry.lookup("init_once").is_some());
176 assert!(registry.lookup("plan").is_some());
177 assert!(registry.lookup("qa").is_some());
178 assert!(registry.lookup("ticket_scan").is_some());
179 assert!(registry.lookup("fix").is_some());
180 assert!(registry.lookup("retest").is_some());
181 assert!(registry.lookup("loop_guard").is_some());
182 assert!(registry.lookup("build").is_some());
183 assert!(registry.lookup("test").is_some());
184 assert!(registry.lookup("lint").is_some());
185 assert!(registry.lookup("implement").is_some());
186 assert!(registry.lookup("review").is_some());
187 assert!(registry.lookup("git_ops").is_some());
188 assert!(registry.lookup("qa_doc_gen").is_some());
189 assert!(registry.lookup("qa_testing").is_some());
190 assert!(registry.lookup("ticket_fix").is_some());
191 assert!(registry.lookup("doc_governance").is_some());
192 assert!(registry.lookup("align_tests").is_some());
193 assert!(registry.lookup("self_test").is_some());
194 assert!(registry.lookup("self_restart").is_some());
195 assert!(registry.lookup("smoke_chain").is_some());
196 assert!(registry.lookup("evaluate").is_some());
197 assert!(registry.lookup("item_select").is_some());
198 }
199
200 #[test]
201 fn framework_builtins_detected() {
202 let registry = StepConventionRegistry::builtin();
203 for name in &[
204 "init_once",
205 "loop_guard",
206 "ticket_scan",
207 "self_test",
208 "self_restart",
209 "item_select",
210 ] {
211 assert!(
212 registry.is_known_builtin(name),
213 "{name} should be a known builtin"
214 );
215 }
216 for name in &["plan", "qa", "fix", "qa_doc_gen", "ticket_fix"] {
218 assert!(
219 !registry.is_known_builtin(name),
220 "{name} should NOT be a builtin"
221 );
222 }
223 }
224
225 #[test]
226 fn scope_defaults() {
227 let registry = StepConventionRegistry::builtin();
228 assert_eq!(registry.default_scope("plan"), StepScope::Task);
229 assert_eq!(registry.default_scope("qa"), StepScope::Item);
230 assert_eq!(registry.default_scope("qa_testing"), StepScope::Item);
231 assert_eq!(registry.default_scope("ticket_fix"), StepScope::Item);
232 assert_eq!(registry.default_scope("fix"), StepScope::Item);
233 assert_eq!(registry.default_scope("retest"), StepScope::Item);
234 assert_eq!(registry.default_scope("implement"), StepScope::Task);
235 assert_eq!(registry.default_scope("my_custom_step"), StepScope::Task);
237 }
238
239 #[test]
240 fn qa_step_has_captures_and_post_actions() {
241 let registry = StepConventionRegistry::builtin();
242 let qa = registry.lookup("qa").unwrap();
243 assert!(qa.collect_artifacts);
244 assert_eq!(qa.captures.len(), 1);
245 assert_eq!(qa.captures[0].var, "qa_failed");
246 assert_eq!(qa.captures[0].source, CaptureSource::FailedFlag);
247 assert_eq!(qa.post_actions.len(), 1);
248 assert_eq!(qa.post_actions[0], PostAction::CreateTicket);
249 }
250
251 #[test]
252 fn fix_step_has_captures() {
253 let registry = StepConventionRegistry::builtin();
254 let fix = registry.lookup("fix").unwrap();
255 assert!(!fix.collect_artifacts);
256 assert_eq!(fix.captures.len(), 1);
257 assert_eq!(fix.captures[0].var, "fix_success");
258 assert_eq!(fix.captures[0].source, CaptureSource::SuccessFlag);
259 assert!(fix.post_actions.is_empty());
260 }
261
262 #[test]
263 fn unknown_step_returns_none() {
264 let registry = StepConventionRegistry::builtin();
265 assert!(registry.lookup("my_custom_deploy").is_none());
266 }
267}