directiva 0.2.0

A tiny, paste-friendly directive mini-language: ACTION:[<KIND>]NAME[@PATH][=NOTE]
Documentation
//! [`Ladder`] — a tiny, fully decoupled ordered-level monoid.
//!
//! It references neither [`Directive`](crate::core::Directive) nor [`Action`](crate::core::Action):
//! it's "convenient ladderization" usable for *any* ordered set — linter severities, a markup
//! workflow `draft < review < final`, anything. Steps form a commutative, associative, saturating
//! monoid: combine several deltas by summing, then project once with [`Ladder::step`].

/// An ordered set of levels, most-severe first (index `0`).
#[derive(Clone, Debug, PartialEq, Eq)]
pub struct Ladder<S> {
    levels: Vec<S>,
}

impl<S: PartialEq + Clone> Ladder<S> {
    /// Build a ladder from levels ordered **most-severe first** (`[Error, Warning, Info]`).
    #[must_use]
    pub fn new(most_to_least_severe: Vec<S>) -> Self {
        Self {
            levels: most_to_least_severe,
        }
    }

    /// The rung index of `level`, or `None` if it isn't on this ladder.
    #[must_use]
    pub fn position(&self, level: &S) -> Option<usize> {
        self.levels.iter().position(|l| l == level)
    }

    /// All levels, most-severe first.
    #[must_use]
    pub fn levels(&self) -> &[S] {
        &self.levels
    }

    /// Move `from` by `delta` rungs, **saturating** at both ends. `delta > 0` steps toward the
    /// *less* severe end (higher index); `delta < 0` toward the *more* severe end. A `level` not on
    /// the ladder is returned unchanged.
    #[must_use]
    pub fn step(&self, from: &S, delta: i32) -> S {
        let Some(i) = self.position(from) else {
            return from.clone();
        };
        let last = self.levels.len() - 1; // non-empty: `from` was found
        let mag = usize::try_from(delta.unsigned_abs()).unwrap_or(usize::MAX);
        let idx = if delta >= 0 {
            i.saturating_add(mag).min(last)
        } else {
            i.saturating_sub(mag)
        };
        self.levels[idx].clone()
    }
}

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

    fn severity() -> Ladder<&'static str> {
        Ladder::new(vec!["error", "warning", "info"])
    }

    #[test]
    fn steps_down_saturate() {
        assert_eq!(severity().step(&"error", 2), "info");
    }

    #[test]
    fn over_step_down_clamps() {
        assert_eq!(severity().step(&"info", 5), "info");
    }

    #[test]
    fn over_step_up_clamps() {
        assert_eq!(severity().step(&"error", -3), "error");
    }

    // additive monoid: summed deltas, applied once
    #[test]
    fn escalate_then_deescalate_is_noop() {
        let l = severity();
        let total = -1 + 1;
        assert_eq!(l.step(&"warning", total), "warning");
    }

    // not severity-specific
    #[test]
    fn markup_ladder() {
        let l = Ladder::new(vec!["draft", "review", "final"]);
        assert_eq!(l.step(&"draft", 1), "review");
        assert_eq!(l.step(&"final", -2), "draft");
    }

    #[test]
    fn unknown_level_unchanged() {
        let l = severity();
        assert_eq!(l.position(&"bogus"), None);
        assert_eq!(l.step(&"bogus", 1), "bogus");
    }
}