1use std::fmt;
28
29#[derive(Clone, Debug, PartialEq, Eq)]
34pub enum PromptPhase {
35 Planning,
37 Development,
39 Commit,
41 Review,
43 Fix,
45 ConflictResolution {
50 phase: String,
52 },
53}
54
55#[derive(Clone, Debug, PartialEq, Eq)]
59pub enum RetryMode {
60 Normal,
62 SameAgent {
64 count: u32,
66 },
67 Xsd {
69 count: u32,
71 },
72}
73
74#[derive(Clone, Debug, PartialEq, Eq)]
85pub struct PromptScopeKey {
86 phase: PromptPhase,
88 iteration: u32,
90 pass: Option<u32>,
92 attempt: Option<u32>,
94 continuation: Option<u32>,
96 retry_mode: RetryMode,
98 recovery_epoch: u32,
101}
102
103impl PromptScopeKey {
104 #[must_use]
106 pub const fn for_planning(iteration: u32, retry_mode: RetryMode, recovery_epoch: u32) -> Self {
107 Self {
108 phase: PromptPhase::Planning,
109 iteration,
110 pass: None,
111 attempt: None,
112 continuation: None,
113 retry_mode,
114 recovery_epoch,
115 }
116 }
117
118 #[must_use]
123 pub const fn for_development(
124 iteration: u32,
125 continuation: Option<u32>,
126 retry_mode: RetryMode,
127 recovery_epoch: u32,
128 ) -> Self {
129 Self {
130 phase: PromptPhase::Development,
131 iteration,
132 pass: None,
133 attempt: None,
134 continuation,
135 retry_mode,
136 recovery_epoch,
137 }
138 }
139
140 #[must_use]
142 pub const fn for_commit(
143 iteration: u32,
144 attempt: u32,
145 retry_mode: RetryMode,
146 recovery_epoch: u32,
147 ) -> Self {
148 Self {
149 phase: PromptPhase::Commit,
150 iteration,
151 pass: None,
152 attempt: Some(attempt),
153 continuation: None,
154 retry_mode,
155 recovery_epoch,
156 }
157 }
158
159 #[must_use]
161 pub const fn for_review(pass: u32, retry_mode: RetryMode, recovery_epoch: u32) -> Self {
162 Self {
163 phase: PromptPhase::Review,
164 iteration: 0,
165 pass: Some(pass),
166 attempt: None,
167 continuation: None,
168 retry_mode,
169 recovery_epoch,
170 }
171 }
172
173 #[must_use]
175 pub const fn for_fix(pass: u32, retry_mode: RetryMode, recovery_epoch: u32) -> Self {
176 Self {
177 phase: PromptPhase::Fix,
178 iteration: 0,
179 pass: Some(pass),
180 attempt: None,
181 continuation: None,
182 retry_mode,
183 recovery_epoch,
184 }
185 }
186
187 #[must_use]
200 pub fn for_conflict_resolution(phase: &str, recovery_epoch: u32) -> Self {
201 Self {
202 phase: PromptPhase::ConflictResolution {
203 phase: phase.to_lowercase(),
204 },
205 iteration: 0,
206 pass: None,
207 attempt: None,
208 continuation: None,
209 retry_mode: RetryMode::Normal,
210 recovery_epoch,
211 }
212 }
213}
214
215impl fmt::Display for PromptScopeKey {
231 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
232 let base = match &self.phase {
233 PromptPhase::Planning => format!("planning_{}", self.iteration),
234 PromptPhase::Development => self.continuation.map_or_else(
235 || format!("development_{}", self.iteration),
236 |c| format!("development_{}_continuation_{}", self.iteration, c),
237 ),
238 PromptPhase::Commit => format!(
239 "commit_message_attempt_iter{}_{}",
240 self.iteration,
241 self.attempt.unwrap_or(1)
242 ),
243 PromptPhase::Review => format!("review_{}", self.pass.unwrap_or(1)),
244 PromptPhase::Fix => format!("fix_{}", self.pass.unwrap_or(1)),
245 PromptPhase::ConflictResolution { phase } => {
246 format!("{phase}_conflict_resolution")
247 }
248 };
249 match &self.retry_mode {
250 RetryMode::Normal => write!(f, "{base}"),
251 RetryMode::SameAgent { count } => write!(f, "{base}_same_agent_retry_{count}"),
252 RetryMode::Xsd { count } => write!(f, "{base}_xsd_retry_{count}"),
253 }
254 }
255}
256
257#[cfg(test)]
258mod tests {
259 use super::*;
260
261 #[test]
266 fn planning_normal_key_matches_legacy_format() {
267 let key = PromptScopeKey::for_planning(0, RetryMode::Normal, 0);
268 assert_eq!(key.to_string(), "planning_0");
269 }
270
271 #[test]
272 fn planning_normal_key_iteration_2() {
273 let key = PromptScopeKey::for_planning(2, RetryMode::Normal, 0);
274 assert_eq!(key.to_string(), "planning_2");
275 }
276
277 #[test]
278 fn planning_same_agent_retry_key_matches_legacy_format() {
279 let key = PromptScopeKey::for_planning(0, RetryMode::SameAgent { count: 2 }, 0);
280 assert_eq!(key.to_string(), "planning_0_same_agent_retry_2");
281 }
282
283 #[test]
288 fn development_normal_key_matches_legacy_format() {
289 let key = PromptScopeKey::for_development(0, None, RetryMode::Normal, 0);
290 assert_eq!(key.to_string(), "development_0");
291 }
292
293 #[test]
294 fn development_continuation_key_matches_legacy_format() {
295 let key = PromptScopeKey::for_development(0, Some(3), RetryMode::Normal, 0);
296 assert_eq!(key.to_string(), "development_0_continuation_3");
297 }
298
299 #[test]
300 fn development_same_agent_retry_key_matches_legacy_format() {
301 let key = PromptScopeKey::for_development(2, None, RetryMode::SameAgent { count: 1 }, 0);
302 assert_eq!(key.to_string(), "development_2_same_agent_retry_1");
303 }
304
305 #[test]
310 fn commit_normal_key_matches_legacy_format() {
311 let key = PromptScopeKey::for_commit(0, 1, RetryMode::Normal, 0);
312 assert_eq!(key.to_string(), "commit_message_attempt_iter0_1");
313 }
314
315 #[test]
316 fn commit_same_agent_retry_key_matches_legacy_format() {
317 let key = PromptScopeKey::for_commit(0, 1, RetryMode::SameAgent { count: 1 }, 0);
318 assert_eq!(
319 key.to_string(),
320 "commit_message_attempt_iter0_1_same_agent_retry_1"
321 );
322 }
323
324 #[test]
325 fn commit_xsd_retry_key_matches_legacy_format() {
326 let key = PromptScopeKey::for_commit(0, 1, RetryMode::Xsd { count: 1 }, 0);
327 assert_eq!(
328 key.to_string(),
329 "commit_message_attempt_iter0_1_xsd_retry_1"
330 );
331 }
332
333 #[test]
338 fn review_normal_key_matches_legacy_format() {
339 let key = PromptScopeKey::for_review(0, RetryMode::Normal, 0);
340 assert_eq!(key.to_string(), "review_0");
341 }
342
343 #[test]
344 fn review_xsd_retry_key_matches_legacy_format() {
345 let key = PromptScopeKey::for_review(1, RetryMode::Xsd { count: 3 }, 0);
347 assert_eq!(key.to_string(), "review_1_xsd_retry_3");
348 }
349
350 #[test]
351 fn review_same_agent_retry_key_matches_legacy_format() {
352 let key = PromptScopeKey::for_review(1, RetryMode::SameAgent { count: 2 }, 0);
353 assert_eq!(key.to_string(), "review_1_same_agent_retry_2");
354 }
355
356 #[test]
361 fn fix_normal_key_matches_legacy_format() {
362 let key = PromptScopeKey::for_fix(1, RetryMode::Normal, 0);
363 assert_eq!(key.to_string(), "fix_1");
364 }
365
366 #[test]
367 fn fix_same_agent_retry_key_matches_legacy_format() {
368 let key = PromptScopeKey::for_fix(1, RetryMode::SameAgent { count: 1 }, 0);
369 assert_eq!(key.to_string(), "fix_1_same_agent_retry_1");
370 }
371
372 #[test]
373 fn fix_xsd_retry_key_matches_legacy_format() {
374 let key = PromptScopeKey::for_fix(1, RetryMode::Xsd { count: 2 }, 0);
375 assert_eq!(key.to_string(), "fix_1_xsd_retry_2");
376 }
377
378 #[test]
383 fn recovery_epoch_not_in_display_string() {
384 let key_epoch_0 = PromptScopeKey::for_planning(1, RetryMode::Normal, 0);
387 let key_epoch_1 = PromptScopeKey::for_planning(1, RetryMode::Normal, 1);
388 assert_eq!(
389 key_epoch_0.to_string(),
390 key_epoch_1.to_string(),
391 "recovery_epoch must not affect Display string for checkpoint compat"
392 );
393 }
394
395 #[test]
396 fn keys_are_unique_across_phases() {
397 let planning = PromptScopeKey::for_planning(1, RetryMode::Normal, 0).to_string();
398 let development =
399 PromptScopeKey::for_development(1, None, RetryMode::Normal, 0).to_string();
400 let commit = PromptScopeKey::for_commit(1, 1, RetryMode::Normal, 0).to_string();
401 let review = PromptScopeKey::for_review(1, RetryMode::Normal, 0).to_string();
402 let fix = PromptScopeKey::for_fix(1, RetryMode::Normal, 0).to_string();
403
404 let all = [&planning, &development, &commit, &review, &fix];
405 for (i, k1) in all.iter().enumerate() {
406 for (j, k2) in all.iter().enumerate() {
407 if i != j {
408 assert_ne!(k1, k2, "Keys for different phases must be unique");
409 }
410 }
411 }
412 }
413
414 #[test]
415 fn keys_are_unique_across_retry_modes() {
416 let normal = PromptScopeKey::for_planning(1, RetryMode::Normal, 0).to_string();
417 let same_agent =
418 PromptScopeKey::for_planning(1, RetryMode::SameAgent { count: 1 }, 0).to_string();
419 assert_ne!(normal, same_agent);
420 }
421
422 #[test]
423 fn keys_are_unique_across_iterations() {
424 let iter1 = PromptScopeKey::for_planning(1, RetryMode::Normal, 0).to_string();
425 let iter2 = PromptScopeKey::for_planning(2, RetryMode::Normal, 0).to_string();
426 assert_ne!(iter1, iter2);
427 }
428
429 #[test]
434 fn development_keys_are_unique_across_iterations() {
435 let iter1 = PromptScopeKey::for_development(1, None, RetryMode::Normal, 0).to_string();
436 let iter2 = PromptScopeKey::for_development(2, None, RetryMode::Normal, 0).to_string();
437 assert_ne!(
438 iter1, iter2,
439 "Development keys must differ across iterations to prevent stale replay. \
440 iter1='{iter1}', iter2='{iter2}'"
441 );
442 }
443
444 #[test]
453 fn commit_keys_are_unique_across_iterations_same_attempt() {
454 let iter1_attempt1 = PromptScopeKey::for_commit(1, 1, RetryMode::Normal, 0).to_string();
456 let iter2_attempt1 = PromptScopeKey::for_commit(2, 1, RetryMode::Normal, 0).to_string();
457 assert_ne!(
458 iter1_attempt1, iter2_attempt1,
459 "Commit keys must differ across iterations even when attempt number is the same. \
460 iter1/attempt1 = '{iter1_attempt1}', iter2/attempt1 = '{iter2_attempt1}'"
461 );
462 }
463
464 #[test]
469 fn test_conflict_resolution_key_format_matches_legacy_raw_string() {
470 let key = PromptScopeKey::for_conflict_resolution("planning", 0);
473 assert_eq!(key.to_string(), "planning_conflict_resolution");
474 }
475
476 #[test]
477 fn test_conflict_resolution_key_for_different_phases() {
478 assert_eq!(
479 PromptScopeKey::for_conflict_resolution("development", 0).to_string(),
480 "development_conflict_resolution"
481 );
482 assert_eq!(
483 PromptScopeKey::for_conflict_resolution("RebaseOnly", 0).to_string(),
484 "rebaseonly_conflict_resolution"
485 );
486 }
487
488 #[test]
489 fn test_conflict_resolution_key_lowercases_phase() {
490 let upper = PromptScopeKey::for_conflict_resolution("PLANNING", 0).to_string();
491 let lower = PromptScopeKey::for_conflict_resolution("planning", 0).to_string();
492 assert_eq!(upper, lower);
493 }
494
495 #[test]
496 fn test_conflict_resolution_key_recovery_epoch_not_in_display() {
497 let key_epoch0 = PromptScopeKey::for_conflict_resolution("planning", 0);
498 let key_epoch1 = PromptScopeKey::for_conflict_resolution("planning", 1);
499 assert_eq!(
500 key_epoch0.to_string(),
501 key_epoch1.to_string(),
502 "recovery_epoch must not affect Display string for checkpoint compat"
503 );
504 }
505
506 #[test]
507 fn test_conflict_resolution_key_is_unique_from_pipeline_phase_keys() {
508 let conflict_key = PromptScopeKey::for_conflict_resolution("planning", 0).to_string();
509 let planning_key = PromptScopeKey::for_planning(1, RetryMode::Normal, 0).to_string();
510 let development_key =
511 PromptScopeKey::for_development(1, None, RetryMode::Normal, 0).to_string();
512 assert_ne!(conflict_key, planning_key);
514 assert_ne!(conflict_key, development_key);
515 assert!(conflict_key.ends_with("_conflict_resolution"));
516 }
517}