main
John Lauer Publish 0.1.0 55a9481 2d ago
//! The flow file (flows/flow.json, compiled in): the ordered steps, the artifacts that prove each one
//! is done, and what the AI must do in it. `next` names the first step whose artifacts are missing.
//! The binary does the deterministic parts; the AI does the thinking the `do` text asks for.
use serde::Deserialize;
use std::path::Path;

#[derive(Deserialize, Clone)]
pub struct Step { pub name: String, pub done: Vec<String>, #[serde(rename = "do")] pub todo: String }

#[derive(Deserialize)]
#[allow(dead_code)]
pub struct Flow { pub name: String, pub description: String, pub steps: Vec<Step> }

pub fn load() -> Flow { serde_json::from_str(include_str!("../flows/flow.json")).expect("flows/flow.json is valid") }

/// One artifact pattern, relative to the flow folder; `*` matches within the last path segment.
pub fn exists(flow_dir: &Path, pat: &str) -> bool {
    let full = flow_dir.join(pat);
    let name = full.file_name().and_then(|n| n.to_str()).unwrap_or("");
    if !name.contains('*') { return full.exists(); }
    let (pre, post) = name.split_once('*').unwrap_or((name, ""));
    let dir = full.parent().unwrap_or(flow_dir);
    std::fs::read_dir(dir).map(|rd| rd.flatten().any(|e| {
        let n = e.file_name().to_string_lossy().to_string();
        n.starts_with(pre) && n.ends_with(post) && n.len() >= pre.len() + post.len()
    })).unwrap_or(false)
}

pub fn current<'a>(flow: &'a Flow, flow_dir: &Path) -> Option<&'a Step> {
    flow.steps.iter().find(|s| !s.done.iter().all(|p| exists(flow_dir, p)))
}

#[cfg(test)]
mod tests {
    use super::*;

    #[test]
    fn current_step_is_the_first_with_missing_artifacts() {
        if load().name != "example" { return; } // written for the template flow
        let d = std::env::temp_dir().join(format!("flow-test-{}", std::process::id()));
        std::fs::create_dir_all(d.join("work")).unwrap();
        std::fs::write(d.join("state.json"), "{}").unwrap();
        let f = load();
        assert_eq!(current(&f, &d).map(|s| s.name.as_str()), Some("work"));
        std::fs::write(d.join("work/a.json"), "{}").unwrap();
        assert!(exists(&d, "work/*.json"));
        assert_eq!(current(&f, &d).map(|s| s.name.as_str()), Some("report"));
        std::fs::remove_dir_all(&d).ok();
    }
}