use std::collections::{HashMap, HashSet, VecDeque};
use actionqueue_core::ids::TaskId;
pub const DEFAULT_MAX_DEPTH: usize = 8;
#[derive(Debug, Clone, PartialEq, Eq)]
pub enum HierarchyError {
OrphanPrevention {
child: TaskId,
parent: TaskId,
},
DepthLimitExceeded {
child: TaskId,
parent: TaskId,
depth: usize,
limit: usize,
},
}
impl std::fmt::Display for HierarchyError {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
match self {
HierarchyError::OrphanPrevention { child, parent } => {
write!(
f,
"cannot register child {child} under terminal parent {parent} (orphan \
prevention)"
)
}
HierarchyError::DepthLimitExceeded { child, parent, depth, limit } => {
write!(
f,
"registering child {child} under {parent} would reach depth {depth} (limit: \
{limit})"
)
}
}
}
}
impl std::error::Error for HierarchyError {}
#[derive(Debug, Default, Clone, PartialEq, Eq)]
pub struct HierarchyTracker {
children: HashMap<TaskId, HashSet<TaskId>>,
parents: HashMap<TaskId, TaskId>,
terminal_tasks: HashSet<TaskId>,
max_depth: usize,
}
impl HierarchyTracker {
pub fn new() -> Self {
Self::with_max_depth(DEFAULT_MAX_DEPTH)
}
pub fn with_max_depth(max_depth: usize) -> Self {
Self {
children: HashMap::new(),
parents: HashMap::new(),
terminal_tasks: HashSet::new(),
max_depth,
}
}
pub fn register_child(&mut self, parent: TaskId, child: TaskId) -> Result<(), HierarchyError> {
if self.terminal_tasks.contains(&parent) {
return Err(HierarchyError::OrphanPrevention { child, parent });
}
let child_depth = self.depth(parent) + 1;
if child_depth > self.max_depth {
return Err(HierarchyError::DepthLimitExceeded {
child,
parent,
depth: child_depth,
limit: self.max_depth,
});
}
self.parents.insert(child, parent);
self.children.entry(parent).or_default().insert(child);
Ok(())
}
pub fn children_of(&self, parent: TaskId) -> impl Iterator<Item = TaskId> + '_ {
self.children.get(&parent).into_iter().flat_map(|s| s.iter().copied())
}
pub fn parent_of(&self, child: TaskId) -> Option<TaskId> {
self.parents.get(&child).copied()
}
#[must_use]
pub fn has_children(&self, task_id: TaskId) -> bool {
self.children.contains_key(&task_id)
}
#[must_use]
pub fn is_terminal(&self, task_id: TaskId) -> bool {
self.terminal_tasks.contains(&task_id)
}
pub fn mark_terminal(&mut self, task_id: TaskId) {
self.terminal_tasks.insert(task_id);
}
pub fn depth(&self, task_id: TaskId) -> usize {
let mut depth = 0usize;
let mut current = task_id;
while let Some(&parent) = self.parents.get(¤t) {
depth += 1;
current = parent;
}
depth
}
pub fn gc_subtree(&mut self, task_id: TaskId) {
let mut to_remove = Vec::new();
let mut queue = VecDeque::new();
queue.push_back(task_id);
while let Some(current) = queue.pop_front() {
to_remove.push(current);
if let Some(children) = self.children.get(¤t) {
for &child in children {
queue.push_back(child);
}
}
}
for id in to_remove {
self.children.remove(&id);
self.parents.remove(&id);
self.terminal_tasks.remove(&id);
}
}
pub fn collect_cancellation_cascade(&self, task_id: TaskId) -> Vec<TaskId> {
let mut result = Vec::new();
let mut queue = VecDeque::new();
if let Some(children) = self.children.get(&task_id) {
for &child in children {
queue.push_back(child);
}
}
while let Some(current) = queue.pop_front() {
if self.terminal_tasks.contains(¤t) {
} else {
result.push(current);
}
if let Some(children) = self.children.get(¤t) {
for &child in children {
queue.push_back(child);
}
}
}
result
}
}
#[cfg(test)]
mod tests {
use actionqueue_core::ids::TaskId;
use super::*;
fn tid(n: u128) -> TaskId {
TaskId::from_uuid(uuid::Uuid::from_u128(n))
}
#[test]
fn new_tracker_has_no_children() {
let tracker = HierarchyTracker::new();
assert!(!tracker.has_children(tid(1)));
assert!(tracker.parent_of(tid(2)).is_none());
assert_eq!(tracker.depth(tid(1)), 0);
}
#[test]
fn register_child_records_relationship() {
let mut tracker = HierarchyTracker::new();
tracker.register_child(tid(1), tid(2)).expect("no error");
assert!(tracker.has_children(tid(1)));
assert_eq!(tracker.parent_of(tid(2)), Some(tid(1)));
assert_eq!(tracker.depth(tid(2)), 1);
}
#[test]
fn depth_increases_down_chain() {
let mut tracker = HierarchyTracker::new();
tracker.register_child(tid(1), tid(2)).expect("no error");
tracker.register_child(tid(2), tid(3)).expect("no error");
tracker.register_child(tid(3), tid(4)).expect("no error");
assert_eq!(tracker.depth(tid(1)), 0);
assert_eq!(tracker.depth(tid(2)), 1);
assert_eq!(tracker.depth(tid(3)), 2);
assert_eq!(tracker.depth(tid(4)), 3);
}
#[test]
fn orphan_prevention_rejects_child_of_terminal() {
let mut tracker = HierarchyTracker::new();
tracker.mark_terminal(tid(1));
let err = tracker.register_child(tid(1), tid(2)).expect_err("should fail");
assert!(matches!(err, HierarchyError::OrphanPrevention { child, parent }
if child == tid(2) && parent == tid(1)));
}
#[test]
fn depth_limit_rejects_excessive_nesting() {
let mut tracker = HierarchyTracker::with_max_depth(2);
tracker.register_child(tid(1), tid(2)).expect("depth 1 OK");
tracker.register_child(tid(2), tid(3)).expect("depth 2 OK");
let err = tracker.register_child(tid(3), tid(4)).expect_err("depth 3 exceeds limit 2");
assert!(matches!(err, HierarchyError::DepthLimitExceeded { depth: 3, limit: 2, .. }));
}
#[test]
fn cascade_returns_non_terminal_descendants() {
let mut tracker = HierarchyTracker::new();
tracker.register_child(tid(1), tid(2)).expect("no error");
tracker.register_child(tid(1), tid(3)).expect("no error");
tracker.register_child(tid(2), tid(4)).expect("no error");
let cascade = tracker.collect_cancellation_cascade(tid(1));
assert!(cascade.contains(&tid(2)));
assert!(cascade.contains(&tid(3)));
assert!(cascade.contains(&tid(4)));
assert_eq!(cascade.len(), 3);
}
#[test]
fn cascade_excludes_terminal_descendants() {
let mut tracker = HierarchyTracker::new();
tracker.register_child(tid(1), tid(2)).expect("no error");
tracker.register_child(tid(1), tid(3)).expect("no error");
tracker.mark_terminal(tid(2));
let cascade = tracker.collect_cancellation_cascade(tid(1));
assert!(!cascade.contains(&tid(2)), "tid(2) is terminal, excluded");
assert!(cascade.contains(&tid(3)));
assert_eq!(cascade.len(), 1);
}
#[test]
fn cascade_traverses_through_terminal_intermediary() {
let mut tracker = HierarchyTracker::new();
tracker.register_child(tid(1), tid(2)).expect("no error");
tracker.register_child(tid(2), tid(3)).expect("no error");
tracker.mark_terminal(tid(2));
let cascade = tracker.collect_cancellation_cascade(tid(1));
assert!(!cascade.contains(&tid(2)), "terminal");
assert!(cascade.contains(&tid(3)), "tid(3) is non-terminal leaf under terminal tid(2)");
}
#[test]
fn cascade_returns_empty_for_leaf_task() {
let tracker = HierarchyTracker::new();
assert!(tracker.collect_cancellation_cascade(tid(99)).is_empty());
}
#[test]
fn cascade_self_quenches_after_mark_terminal() {
let mut tracker = HierarchyTracker::new();
tracker.register_child(tid(1), tid(2)).expect("no error");
let first = tracker.collect_cancellation_cascade(tid(1));
assert_eq!(first, vec![tid(2)]);
tracker.mark_terminal(tid(2));
let second = tracker.collect_cancellation_cascade(tid(1));
assert!(second.is_empty(), "self-quenches after mark_terminal");
}
#[test]
fn bootstrap_registration_succeeds_with_empty_terminal_set() {
let mut tracker = HierarchyTracker::new();
tracker.register_child(tid(1), tid(2)).expect("no error during bootstrap");
tracker.mark_terminal(tid(1));
tracker.mark_terminal(tid(2));
tracker.register_child(tid(1), tid(3)).expect_err("parent is terminal");
}
#[test]
fn gc_subtree_removes_terminal_subtree() {
let mut tracker = HierarchyTracker::new();
tracker.register_child(tid(1), tid(2)).expect("no error");
tracker.register_child(tid(2), tid(3)).expect("no error");
tracker.mark_terminal(tid(1));
tracker.mark_terminal(tid(2));
tracker.mark_terminal(tid(3));
assert!(tracker.collect_cancellation_cascade(tid(1)).is_empty());
tracker.gc_subtree(tid(1));
assert!(!tracker.has_children(tid(1)));
assert!(!tracker.has_children(tid(2)));
assert!(tracker.parent_of(tid(2)).is_none());
assert!(tracker.parent_of(tid(3)).is_none());
assert!(!tracker.is_terminal(tid(1)));
assert!(!tracker.is_terminal(tid(2)));
assert!(!tracker.is_terminal(tid(3)));
}
#[test]
fn gc_subtree_is_idempotent() {
let mut tracker = HierarchyTracker::new();
tracker.register_child(tid(1), tid(2)).expect("no error");
tracker.mark_terminal(tid(1));
tracker.mark_terminal(tid(2));
tracker.gc_subtree(tid(1));
tracker.gc_subtree(tid(1)); }
#[test]
fn max_depth_default_is_eight() {
let tracker = HierarchyTracker::new();
assert_eq!(tracker.max_depth, DEFAULT_MAX_DEPTH);
}
}