Skip to main content

selfware/evolve/
actions.rs

1//! ActionEngine: builds non-mutating evolution action proposals.
2//!
3//! Actions either change the active context (handled elsewhere) or produce a
4//! branch proposal. Branch creation itself is owned by the guarded Git engine.
5
6use anyhow::Result;
7use chrono::Utc;
8
9#[derive(Debug, Clone)]
10pub enum Action {
11    Extend { component: String },
12    Connect { from: String, to: String },
13    BlockEvolution { component: String },
14    Notify { component: String },
15}
16
17pub struct ActionResult {
18    /// Suggested branch name only; no Git branch is created by `propose`.
19    pub branch: Option<String>,
20    pub message: String,
21}
22
23pub struct ActionEngine {
24    // Stateless by design. Guarded mutations live in the dedicated engines.
25}
26
27impl Default for ActionEngine {
28    fn default() -> Self {
29        Self::new()
30    }
31}
32
33impl ActionEngine {
34    pub fn new() -> Self {
35        Self {}
36    }
37
38    pub fn branch_name(action: &Action) -> String {
39        let ts = Utc::now().format("%Y%m%d-%H%M%S");
40        match action {
41            Action::Extend { component } => {
42                format!("evolve/extend-{}-{}", branch_segment(component), ts)
43            }
44            Action::Connect { from, to } => format!(
45                "evolve/connect-{}-{}-{}",
46                branch_segment(from),
47                branch_segment(to),
48                ts
49            ),
50            Action::BlockEvolution { component } => {
51                format!("evolve/block-{}-{}", branch_segment(component), ts)
52            }
53            Action::Notify { component } => {
54                format!("evolve/notify-{}-{}", branch_segment(component), ts)
55            }
56        }
57    }
58
59    /// Build an action proposal.
60    ///
61    /// This does not mutate Git or source. It returns a proposal that must be
62    /// previewed and confirmed through the guarded Git engine.
63    pub fn propose(&self, action: &Action) -> Result<ActionResult> {
64        match action {
65            Action::Extend { component } => Ok(ActionResult {
66                branch: Some(Self::branch_name(action)),
67                message: format!("Proposed an extension branch for {component}"),
68            }),
69            _ => Ok(ActionResult {
70                branch: None,
71                message: "No mutation was performed; this action has no executor".to_string(),
72            }),
73        }
74    }
75}
76
77fn branch_segment(value: &str) -> String {
78    let mut result = String::new();
79    let mut previous_separator = false;
80    for character in value.chars() {
81        let allowed = character.is_ascii_alphanumeric() || matches!(character, '.' | '_' | '-');
82        if allowed {
83            result.push(character.to_ascii_lowercase());
84            previous_separator = false;
85        } else if !previous_separator && !result.is_empty() {
86            result.push('-');
87            previous_separator = true;
88        }
89    }
90    let trimmed = result.trim_matches(['.', '-']).to_string();
91    if trimmed.is_empty() {
92        "component".to_string()
93    } else {
94        trimmed
95    }
96}