use std::fmt;
#[derive(Clone, Debug, PartialEq, Eq)]
pub enum PromptPhase {
Planning,
Development,
Commit,
Review,
Fix,
ConflictResolution {
phase: String,
},
}
#[derive(Clone, Debug, PartialEq, Eq)]
pub enum RetryMode {
Normal,
SameAgent {
count: u32,
},
Xsd {
count: u32,
},
}
#[derive(Clone, Debug, PartialEq, Eq)]
pub struct PromptScopeKey {
phase: PromptPhase,
iteration: u32,
pass: Option<u32>,
attempt: Option<u32>,
continuation: Option<u32>,
retry_mode: RetryMode,
recovery_epoch: u32,
}
impl PromptScopeKey {
#[must_use]
pub const fn for_planning(iteration: u32, retry_mode: RetryMode, recovery_epoch: u32) -> Self {
Self {
phase: PromptPhase::Planning,
iteration,
pass: None,
attempt: None,
continuation: None,
retry_mode,
recovery_epoch,
}
}
#[must_use]
pub const fn for_development(
iteration: u32,
continuation: Option<u32>,
retry_mode: RetryMode,
recovery_epoch: u32,
) -> Self {
Self {
phase: PromptPhase::Development,
iteration,
pass: None,
attempt: None,
continuation,
retry_mode,
recovery_epoch,
}
}
#[must_use]
pub const fn for_commit(
iteration: u32,
attempt: u32,
retry_mode: RetryMode,
recovery_epoch: u32,
) -> Self {
Self {
phase: PromptPhase::Commit,
iteration,
pass: None,
attempt: Some(attempt),
continuation: None,
retry_mode,
recovery_epoch,
}
}
#[must_use]
pub const fn for_review(pass: u32, retry_mode: RetryMode, recovery_epoch: u32) -> Self {
Self {
phase: PromptPhase::Review,
iteration: 0,
pass: Some(pass),
attempt: None,
continuation: None,
retry_mode,
recovery_epoch,
}
}
#[must_use]
pub const fn for_fix(pass: u32, retry_mode: RetryMode, recovery_epoch: u32) -> Self {
Self {
phase: PromptPhase::Fix,
iteration: 0,
pass: Some(pass),
attempt: None,
continuation: None,
retry_mode,
recovery_epoch,
}
}
#[must_use]
pub fn for_conflict_resolution(phase: &str, recovery_epoch: u32) -> Self {
Self {
phase: PromptPhase::ConflictResolution {
phase: phase.to_lowercase(),
},
iteration: 0,
pass: None,
attempt: None,
continuation: None,
retry_mode: RetryMode::Normal,
recovery_epoch,
}
}
}
impl fmt::Display for PromptScopeKey {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
let base = match &self.phase {
PromptPhase::Planning => format!("planning_{}", self.iteration),
PromptPhase::Development => self.continuation.map_or_else(
|| format!("development_{}", self.iteration),
|c| format!("development_{}_continuation_{}", self.iteration, c),
),
PromptPhase::Commit => format!(
"commit_message_attempt_iter{}_{}",
self.iteration,
self.attempt.unwrap_or(1)
),
PromptPhase::Review => format!("review_{}", self.pass.unwrap_or(1)),
PromptPhase::Fix => format!("fix_{}", self.pass.unwrap_or(1)),
PromptPhase::ConflictResolution { phase } => {
format!("{phase}_conflict_resolution")
}
};
match &self.retry_mode {
RetryMode::Normal => write!(f, "{base}"),
RetryMode::SameAgent { count } => write!(f, "{base}_same_agent_retry_{count}"),
RetryMode::Xsd { count } => write!(f, "{base}_xsd_retry_{count}"),
}
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn planning_normal_key_matches_legacy_format() {
let key = PromptScopeKey::for_planning(0, RetryMode::Normal, 0);
assert_eq!(key.to_string(), "planning_0");
}
#[test]
fn planning_normal_key_iteration_2() {
let key = PromptScopeKey::for_planning(2, RetryMode::Normal, 0);
assert_eq!(key.to_string(), "planning_2");
}
#[test]
fn planning_same_agent_retry_key_matches_legacy_format() {
let key = PromptScopeKey::for_planning(0, RetryMode::SameAgent { count: 2 }, 0);
assert_eq!(key.to_string(), "planning_0_same_agent_retry_2");
}
#[test]
fn development_normal_key_matches_legacy_format() {
let key = PromptScopeKey::for_development(0, None, RetryMode::Normal, 0);
assert_eq!(key.to_string(), "development_0");
}
#[test]
fn development_continuation_key_matches_legacy_format() {
let key = PromptScopeKey::for_development(0, Some(3), RetryMode::Normal, 0);
assert_eq!(key.to_string(), "development_0_continuation_3");
}
#[test]
fn development_same_agent_retry_key_matches_legacy_format() {
let key = PromptScopeKey::for_development(2, None, RetryMode::SameAgent { count: 1 }, 0);
assert_eq!(key.to_string(), "development_2_same_agent_retry_1");
}
#[test]
fn commit_normal_key_matches_legacy_format() {
let key = PromptScopeKey::for_commit(0, 1, RetryMode::Normal, 0);
assert_eq!(key.to_string(), "commit_message_attempt_iter0_1");
}
#[test]
fn commit_same_agent_retry_key_matches_legacy_format() {
let key = PromptScopeKey::for_commit(0, 1, RetryMode::SameAgent { count: 1 }, 0);
assert_eq!(
key.to_string(),
"commit_message_attempt_iter0_1_same_agent_retry_1"
);
}
#[test]
fn commit_xsd_retry_key_matches_legacy_format() {
let key = PromptScopeKey::for_commit(0, 1, RetryMode::Xsd { count: 1 }, 0);
assert_eq!(
key.to_string(),
"commit_message_attempt_iter0_1_xsd_retry_1"
);
}
#[test]
fn review_normal_key_matches_legacy_format() {
let key = PromptScopeKey::for_review(0, RetryMode::Normal, 0);
assert_eq!(key.to_string(), "review_0");
}
#[test]
fn review_xsd_retry_key_matches_legacy_format() {
let key = PromptScopeKey::for_review(1, RetryMode::Xsd { count: 3 }, 0);
assert_eq!(key.to_string(), "review_1_xsd_retry_3");
}
#[test]
fn review_same_agent_retry_key_matches_legacy_format() {
let key = PromptScopeKey::for_review(1, RetryMode::SameAgent { count: 2 }, 0);
assert_eq!(key.to_string(), "review_1_same_agent_retry_2");
}
#[test]
fn fix_normal_key_matches_legacy_format() {
let key = PromptScopeKey::for_fix(1, RetryMode::Normal, 0);
assert_eq!(key.to_string(), "fix_1");
}
#[test]
fn fix_same_agent_retry_key_matches_legacy_format() {
let key = PromptScopeKey::for_fix(1, RetryMode::SameAgent { count: 1 }, 0);
assert_eq!(key.to_string(), "fix_1_same_agent_retry_1");
}
#[test]
fn fix_xsd_retry_key_matches_legacy_format() {
let key = PromptScopeKey::for_fix(1, RetryMode::Xsd { count: 2 }, 0);
assert_eq!(key.to_string(), "fix_1_xsd_retry_2");
}
#[test]
fn recovery_epoch_not_in_display_string() {
let key_epoch_0 = PromptScopeKey::for_planning(1, RetryMode::Normal, 0);
let key_epoch_1 = PromptScopeKey::for_planning(1, RetryMode::Normal, 1);
assert_eq!(
key_epoch_0.to_string(),
key_epoch_1.to_string(),
"recovery_epoch must not affect Display string for checkpoint compat"
);
}
#[test]
fn keys_are_unique_across_phases() {
let planning = PromptScopeKey::for_planning(1, RetryMode::Normal, 0).to_string();
let development =
PromptScopeKey::for_development(1, None, RetryMode::Normal, 0).to_string();
let commit = PromptScopeKey::for_commit(1, 1, RetryMode::Normal, 0).to_string();
let review = PromptScopeKey::for_review(1, RetryMode::Normal, 0).to_string();
let fix = PromptScopeKey::for_fix(1, RetryMode::Normal, 0).to_string();
let all = [&planning, &development, &commit, &review, &fix];
assert!(all.iter().enumerate().all(|(i, k1)| {
all.iter()
.enumerate()
.filter(|(j, _)| i != *j)
.all(|(_, k2)| k1 != k2)
}));
}
#[test]
fn keys_are_unique_across_retry_modes() {
let normal = PromptScopeKey::for_planning(1, RetryMode::Normal, 0).to_string();
let same_agent =
PromptScopeKey::for_planning(1, RetryMode::SameAgent { count: 1 }, 0).to_string();
assert_ne!(normal, same_agent);
}
#[test]
fn keys_are_unique_across_iterations() {
let iter1 = PromptScopeKey::for_planning(1, RetryMode::Normal, 0).to_string();
let iter2 = PromptScopeKey::for_planning(2, RetryMode::Normal, 0).to_string();
assert_ne!(iter1, iter2);
}
#[test]
fn development_keys_are_unique_across_iterations() {
let iter1 = PromptScopeKey::for_development(1, None, RetryMode::Normal, 0).to_string();
let iter2 = PromptScopeKey::for_development(2, None, RetryMode::Normal, 0).to_string();
assert_ne!(
iter1, iter2,
"Development keys must differ across iterations to prevent stale replay. \
iter1='{iter1}', iter2='{iter2}'"
);
}
#[test]
fn commit_keys_are_unique_across_iterations_same_attempt() {
let iter1_attempt1 = PromptScopeKey::for_commit(1, 1, RetryMode::Normal, 0).to_string();
let iter2_attempt1 = PromptScopeKey::for_commit(2, 1, RetryMode::Normal, 0).to_string();
assert_ne!(
iter1_attempt1, iter2_attempt1,
"Commit keys must differ across iterations even when attempt number is the same. \
iter1/attempt1 = '{iter1_attempt1}', iter2/attempt1 = '{iter2_attempt1}'"
);
}
#[test]
fn test_conflict_resolution_key_format_matches_legacy_raw_string() {
let key = PromptScopeKey::for_conflict_resolution("planning", 0);
assert_eq!(key.to_string(), "planning_conflict_resolution");
}
#[test]
fn test_conflict_resolution_key_for_different_phases() {
assert_eq!(
PromptScopeKey::for_conflict_resolution("development", 0).to_string(),
"development_conflict_resolution"
);
assert_eq!(
PromptScopeKey::for_conflict_resolution("RebaseOnly", 0).to_string(),
"rebaseonly_conflict_resolution"
);
}
#[test]
fn test_conflict_resolution_key_lowercases_phase() {
let upper = PromptScopeKey::for_conflict_resolution("PLANNING", 0).to_string();
let lower = PromptScopeKey::for_conflict_resolution("planning", 0).to_string();
assert_eq!(upper, lower);
}
#[test]
fn test_conflict_resolution_key_recovery_epoch_not_in_display() {
let key_epoch0 = PromptScopeKey::for_conflict_resolution("planning", 0);
let key_epoch1 = PromptScopeKey::for_conflict_resolution("planning", 1);
assert_eq!(
key_epoch0.to_string(),
key_epoch1.to_string(),
"recovery_epoch must not affect Display string for checkpoint compat"
);
}
#[test]
fn test_conflict_resolution_key_is_unique_from_pipeline_phase_keys() {
let conflict_key = PromptScopeKey::for_conflict_resolution("planning", 0).to_string();
let planning_key = PromptScopeKey::for_planning(1, RetryMode::Normal, 0).to_string();
let development_key =
PromptScopeKey::for_development(1, None, RetryMode::Normal, 0).to_string();
assert_ne!(conflict_key, planning_key);
assert_ne!(conflict_key, development_key);
assert!(conflict_key.ends_with("_conflict_resolution"));
}
}