use crate::types::Step;
use std::collections::{HashMap, HashSet};
pub fn ancestors(steps: &[Step], head_id: &str) -> HashSet<String> {
let step_map: HashMap<&str, &Step> = steps.iter().map(|s| (s.step.id.as_str(), s)).collect();
let mut result = HashSet::new();
let mut stack = vec![head_id];
while let Some(id) = stack.pop() {
if result.insert(id.to_string())
&& let Some(step) = step_map.get(id)
{
for parent in &step.step.parents {
stack.push(parent);
}
}
}
result
}
pub fn dead_ends<'a>(steps: &'a [Step], head_id: &str) -> Vec<&'a Step> {
let active = ancestors(steps, head_id);
steps
.iter()
.filter(|s| !active.contains(&s.step.id))
.collect()
}
pub fn filter_by_actor<'a>(steps: &'a [Step], prefix: &str) -> Vec<&'a Step> {
steps
.iter()
.filter(|s| s.step.actor.starts_with(prefix))
.collect()
}
pub fn filter_by_artifact<'a>(steps: &'a [Step], artifact: &str) -> Vec<&'a Step> {
steps
.iter()
.filter(|s| s.change.contains_key(artifact))
.collect()
}
pub fn filter_by_time_range<'a>(steps: &'a [Step], start: &str, end: &str) -> Vec<&'a Step> {
steps
.iter()
.filter(|s| {
let ts = s.step.timestamp.as_str();
ts >= start && ts <= end
})
.collect()
}
pub fn all_artifacts(steps: &[Step]) -> HashSet<&str> {
steps
.iter()
.flat_map(|s| s.change.keys().map(|k| k.as_str()))
.collect()
}
pub fn all_actors(steps: &[Step]) -> HashSet<&str> {
steps.iter().map(|s| s.step.actor.as_str()).collect()
}
pub fn step_index(steps: &[Step]) -> HashMap<&str, &Step> {
steps.iter().map(|s| (s.step.id.as_str(), s)).collect()
}
#[cfg(test)]
mod tests {
use super::*;
use crate::types::Step;
fn make_step(id: &str, actor: &str, parents: &[&str], artifacts: &[&str]) -> Step {
let mut step = Step::new(id, actor, "2026-01-29T10:00:00Z");
for p in parents {
step = step.with_parent(*p);
}
for a in artifacts {
step = step.with_raw_change(*a, "@@");
}
step
}
#[test]
fn test_ancestors_linear() {
let steps = vec![
make_step("s1", "human:a", &[], &["f.rs"]),
make_step("s2", "agent:b", &["s1"], &["f.rs"]),
make_step("s3", "human:a", &["s2"], &["g.rs"]),
];
let anc = ancestors(&steps, "s3");
assert_eq!(anc.len(), 3);
assert!(anc.contains("s1"));
assert!(anc.contains("s2"));
assert!(anc.contains("s3"));
}
#[test]
fn test_ancestors_with_fork() {
let steps = vec![
make_step("s1", "human:a", &[], &["f.rs"]),
make_step("s2", "agent:b", &["s1"], &["f.rs"]),
make_step("s2a", "agent:b", &["s1"], &["f.rs"]), make_step("s3", "human:a", &["s2"], &["g.rs"]),
];
let anc = ancestors(&steps, "s3");
assert_eq!(anc.len(), 3);
assert!(!anc.contains("s2a"));
}
#[test]
fn test_dead_ends() {
let steps = vec![
make_step("s1", "human:a", &[], &["f.rs"]),
make_step("s2", "agent:b", &["s1"], &["f.rs"]),
make_step("s2a", "agent:b", &["s1"], &["f.rs"]), make_step("s3", "human:a", &["s2"], &["g.rs"]),
];
let dead = dead_ends(&steps, "s3");
assert_eq!(dead.len(), 1);
assert_eq!(dead[0].step.id, "s2a");
}
#[test]
fn test_filter_by_actor() {
let steps = vec![
make_step("s1", "human:alex", &[], &["f.rs"]),
make_step("s2", "agent:claude", &["s1"], &["f.rs"]),
make_step("s3", "tool:rustfmt", &["s2"], &["f.rs"]),
make_step("s4", "human:bob", &["s3"], &["f.rs"]),
];
assert_eq!(filter_by_actor(&steps, "human:").len(), 2);
assert_eq!(filter_by_actor(&steps, "agent:").len(), 1);
assert_eq!(filter_by_actor(&steps, "tool:").len(), 1);
}
#[test]
fn test_filter_by_artifact() {
let steps = vec![
make_step("s1", "human:a", &[], &["src/main.rs"]),
make_step("s2", "agent:b", &["s1"], &["src/lib.rs"]),
make_step("s3", "human:a", &["s2"], &["src/main.rs", "src/lib.rs"]),
];
assert_eq!(filter_by_artifact(&steps, "src/main.rs").len(), 2);
assert_eq!(filter_by_artifact(&steps, "src/lib.rs").len(), 2);
}
#[test]
fn test_filter_by_time_range() {
let steps = vec![
Step::new("s1", "human:a", "2026-01-29T10:00:00Z"),
Step::new("s2", "human:a", "2026-01-29T12:00:00Z"),
Step::new("s3", "human:a", "2026-01-29T14:00:00Z"),
];
let filtered = filter_by_time_range(&steps, "2026-01-29T11:00:00Z", "2026-01-29T13:00:00Z");
assert_eq!(filtered.len(), 1);
assert_eq!(filtered[0].step.id, "s2");
}
#[test]
fn test_all_artifacts() {
let steps = vec![
make_step("s1", "human:a", &[], &["a.rs", "b.rs"]),
make_step("s2", "human:a", &["s1"], &["b.rs", "c.rs"]),
];
let arts = all_artifacts(&steps);
assert_eq!(arts.len(), 3);
assert!(arts.contains("a.rs"));
assert!(arts.contains("b.rs"));
assert!(arts.contains("c.rs"));
}
#[test]
fn test_all_actors() {
let steps = vec![
make_step("s1", "human:alex", &[], &["f.rs"]),
make_step("s2", "agent:claude", &["s1"], &["f.rs"]),
make_step("s3", "human:alex", &["s2"], &["f.rs"]),
];
let actors = all_actors(&steps);
assert_eq!(actors.len(), 2);
assert!(actors.contains("human:alex"));
assert!(actors.contains("agent:claude"));
}
#[test]
fn test_step_index() {
let steps = vec![
make_step("s1", "human:a", &[], &["f.rs"]),
make_step("s2", "human:a", &["s1"], &["f.rs"]),
];
let idx = step_index(&steps);
assert_eq!(idx.len(), 2);
assert_eq!(idx["s1"].step.actor, "human:a");
}
}