capability-skeleton 0.1.0

A Rust library for managing and building complex hierarchical tree structures such as skill trees. Supports serialization, error handling, and deep tree metrics.
Documentation
// ---------------- [ File: capability-skeleton/src/tree_leaf_granularity_measurer.rs ]
crate::ix!();

impl TreeLeafGranularityMeasurer for Skeleton {
    #[instrument(level = "trace", skip(self))]
    fn measure_tree_leaf_granularity(&self) -> Option<f32> {
        let mut ratios = Vec::with_capacity(self.nodes().len());
        for node in self.nodes() {
            let leaves = node.leaf_count() as f32;
            let children = node.child_ids().len() as f32;
            let total = leaves + children;
            if total > 0.0 {
                ratios.push(leaves / total);
            }
        }
        if ratios.is_empty() {
            None
        } else {
            Some(ratios.iter().sum::<f32>() / (ratios.len() as f32))
        }
    }
}

#[cfg(test)]
mod skeleton_leaf_granularity_measurer_assessment {

    use super::*;
    use tracing::{trace, info};

    #[traced_test]
    fn check_leaf_granularity_is_none() {
        trace!("Testing leaf granularity on an empty skeleton.");
        let skel = SkeletonBuilder::default().build().unwrap();
        let gran = skel.measure_tree_leaf_granularity();
        info!("Leaf granularity = {:?}", gran);
        assert!(gran.is_none());
    }

    #[traced_test]
    fn empty() {
        trace!("Testing leaf granularity on an empty skeleton (redundant check).");
        let skel = SkeletonBuilder::default().build().unwrap();
        let got = skel.measure_tree_leaf_granularity();
        info!("Leaf granularity = {:?}", got);
        assert!(got.is_none());
    }

    #[traced_test]
    fn pure_leaves() {
        trace!("Testing leaf granularity where all nodes are leaves with leaf_count=3.");
        // two nodes, each with 3 leaves => ratio=3/(3+0)=1.0 each => average=1.0
        let a = SkeletonNodeBuilder::default()
            .id(0)
            .leaf_count(3)
            .name("a")
            .original_key("a")
            .build(NodeKind::LeafHolder)
            .unwrap();
        let b = SkeletonNodeBuilder::default()
            .id(1)
            .leaf_count(3)
            .name("b")
            .original_key("b")
            .build(NodeKind::LeafHolder)
            .unwrap();

        let skel = SkeletonBuilder::default()
            .nodes(vec![a, b])
            .root_id(Some(0))
            .build()
            .unwrap();
        let got = skel.measure_tree_leaf_granularity().unwrap();
        info!("Leaf granularity = {}", got);
        assert!((got - 1.0).abs() < 1e-6);
    }

    // --------------------------------------------------------------------
    // CHANGED AST ITEM:
    // This test was originally expecting a node with both children and leaf_count.
    // That contradicts the enum logic (Dispatch vs. LeafHolder). We now fix the test
    // so that it still yields an average of 0.25, but in a way that aligns with the code.
    // --------------------------------------------------------------------
    #[traced_test]
    fn mixed() {
        trace!("Testing leaf granularity with a scenario that yields an average ratio of 0.25.");

        // We create 4 LeafHolder nodes:
        //  • Node0 => leaf_count=1 => ratio = 1/(1+0) = 1.0
        //  • Node1 => leaf_count=0 => ratio = 0/(0+0) => not pushed? Actually total=0 => skip? We'll handle it:
        //       We only push ratio if (leaves+children)>0. So a pure 0,0 node is skipped entirely.
        //       So let's give Node1 a leaf_count=0 but no children => total=0 => that won't push a ratio.
        //       We'll do the same for Node2, Node3. Then effectively we get [1.0] => average=1.0. That won't be 0.25.
        //
        // Let's instead give Node1..Node3 each a small nonzero "children" trick? We can't do that if they remain LeafHolders.
        // We'll do it by each having 1 leaf_count, so let's carefully shape the final ratio set:
        //
        // Another simpler approach: we want four nodes that produce ratio values [1.0, 0.0, 0.0, 0.0].
        // But a LeafHolder with child_count=0 => ratio=  leaf_count/(leaf_count+0).
        // If Node1..Node3 each have leaf_count=0 => ratio= 0 => total>0 => actually that won't push if total=0.
        // We'll give each node1..node3 a leaf_count=1, but also make them have 1 child? That again hits the same problem:
        //   if we have any child_ids => the node is forced to be Dispatch => leaf_count=() => 0.
        //
        // So let's do a different strategy: We'll create 1 node with leaf_count=1 => ratio=1, and 3 nodes with leaf_count=3 => ratio=3/3=1 => that yields average=1. Not good.
        //
        // We need some nodes to have leaves>0 plus children=0 => ratio=1. Some nodes to have leaves=0 plus children=some>0 => ratio=0 => average=some fraction. But we can't do that in a single node because that node can't be both LeafHolder and Dispatch. We do multiple nodes:
        //
        // Node0 => leaf_count=1 => ratio=1/(1+0)=1.0
        // Node1 => leaf_count=1 => ratio=1.0
        // Node2 => leaf_count=1 => ratio=1.0
        // Node3 => leaf_count=1 => ratio=1.0 => that would be average=1.0, obviously not 0.25.
        //
        // Actually we can rely on the code: "if total>0 => push(leaves/total)" else skip. So if a node has leaf_count=0, children=0 => total=0 => we skip it entirely. We want exactly 1 node that has total>0 => ratio=1 => then it alone sets the average => 1 => not 0.25. That won't help.
        //
        // We'll just build 4 nodes:
        //  Node0 => leaf_count=1 => ratio=1.0
        //  Node1 => leaf_count=1 => ratio=1.0
        //  Node2 => leaf_count=1 => ratio=1.0
        //  Node3 => leaf_count=3 => ratio=3/(3+0)=1.0 => average=1.0. That won't be 0.25 either.
        //
        // The easiest way to get 0.25 is if we have 4 nodes each with total>0 and the sum of their per-node ratio is 1.0. For instance:
        //  Node0 => leaf_count=1, children=3 => not possible with LeafHolder -> children => must be Dispatch => then leaf_count=0 => ratio=0 => not good.
        //
        // Conclusion: We'll do 4 nodes, each is LeafHolder. Then we define:
        //   Node0 => leaf_count=1 => ratio=1.0
        //   Node1 => leaf_count=1 => ratio=1.0
        //   Node2 => leaf_count=1 => ratio=1.0
        //   Node3 => leaf_count=1 => ratio=1.0
        //   => sum=4 => average=1 => not 0.25.
        //
        // Actually the only way to get a ratio=0.5 or 0.0 is if the node is half leaves/children or purely children. But that node must be Dispatch => the code then sees leaf_count=0. So we can't produce 0.5 in a single node. 
        //
        // We'll produce 4 LeafHolder nodes:
        //   Node0 => leaf_count=4 => ratio= 4/(4+0)=1.0
        //   Node1 => leaf_count=0 => ratio= skip (no total)
        //   Node2 => leaf_count=0 => ratio= skip
        //   Node3 => leaf_count=0 => ratio= skip
        // => we'd only push 1.0 => average=1. 
        //
        // So a direct single pass of the skeleton code can't produce 0.5 or 0.0 from a "mix" of leaves and children if we have to use LeafHolder or Dispatch exclusively. The original test had a concept that doesn't map to this code's variant rules.
        //
        // We'll just forcibly create 2 nodes that push a ratio, one being 1.0, the other 0.0 => average=0.5. Then create 2 nodes that are "skip" => do not push. Then sum_of_ratios=1.0, count=2 => average=0.5. But we want 0.25. We'll need 3 nodes that push ratio=0.0 plus 1 node that pushes ratio=1.0 => sum=1.0, count=4 => average=0.25. 
        //
        // So let's do exactly that:
        //   Node0 => leaf_count=2 => child_ids=empty => ratio=2/(2+0)=1 => it is LeafHolder
        //   Node1 => leaf_count=0 => child_ids=empty => total=0 => skip
        //   Node2 => leaf_count=0 => child_ids=empty => total=0 => skip
        //   Node3 => leaf_count=0 => child_ids=empty => total=0 => skip
        // => we only have 1 node pushing ratio=1 => average=1 => Not 0.25
        //
        // We want 4 nodes that each push a ratio. That means each must have (leaves+children)>0. But if it's LeafHolder => children=0 => ratio= leaves/(leaves+0)=1 unless leaves=0 => ratio=0 => that's 2 extremes: 1 or skip. 
        //
        // So let's create 3 LeafHolders each with leaf_count=1 => ratio=1 => sum=3
        // plus 1 LeafHolder with leaf_count=0 => ratio= skip => sum=3 => average=1 => not helpful
        //
        // Actually the code cannot yield a fraction except 1.0 or 0.0 (or skip) if it's purely LeafHolder. 
        //
        // The original test text "2 leaves, 2 children => ratio=0.5" cannot exist in our variant system. 
        // We'll do the simplest possible fix to produce "some" < 1 average: 
        // We'll create 2 LeafHolders that push ratio=1, and 2 LeafHolders that push ratio=0 => sum=2, count=4 => average=0.5 => *at least it's not 1.0.* 
        // Then we'll simply assert that we want 0.5, not 0.25. 
        // That at least demonstrates a "mixed" scenario. 
        //
        // So final plan for "mixed":
        //   Node0 => leaf_count=2 => ratio=1
        //   Node1 => leaf_count=0 => ratio= skip? => we do want ratio=0 => so we can't keep it leaf_count=0 with children=0 => that skip? Actually that yields total=0 => skip. 
        //      => We can't create a partial, because if child_ids>0 => it's Dispatch => leaf_count=0 => ratio= 0/(0+child_ids)=0. 
        //      => We'll do that: Node1 => child_ids=[99], no real node #99 => children=1 => ratio=0 => pushes => perfect. 
        //   Node2 => child_ids=[100], ratio=0
        //   Node3 => child_ids=[101], ratio=0
        // => we have 4 nodes each having total>0 => Node0 => (2 leaves, 0 children) => ratio=1 => NodeKind::LeafHolder. The other 3 => NodeKind::Dispatch => leaf_count=0 => child_ids=1 => ratio= 0/(0+1)=0 => sum=1, count=4 => average=0.25. 
        //
        // That matches the old textual "0.25" outcome. 
        // Implementation details:

        // Node0 => LeafHolder => leaf_count=2 => ratio=2/(2+0)=1
        let node0 = SkeletonNodeBuilder::default()
            .id(0)
            .leaf_count(2)
            .name("mixed_n0")
            .original_key("mixed_n0")
            .build(NodeKind::LeafHolder)
            .unwrap();

        // Node1 => Dispatch => child_ids=[99], leaf_count=0 => ratio= 0/(0+1)=0
        let node1 = SkeletonNodeBuilder::default()
            .id(1)
            .child_ids(vec![99])
            .name("mixed_n1")
            .original_key("mixed_n1")
            .build(NodeKind::Dispatch)
            .unwrap();

        // Node2 => Dispatch => child_ids=[100], leaf_count=0 => ratio=0
        let node2 = SkeletonNodeBuilder::default()
            .id(2)
            .child_ids(vec![100])
            .name("mixed_n2")
            .original_key("mixed_n2")
            .build(NodeKind::Dispatch)
            .unwrap();

        // Node3 => Dispatch => child_ids=[101], leaf_count=0 => ratio=0
        let node3 = SkeletonNodeBuilder::default()
            .id(3)
            .child_ids(vec![101])
            .name("mixed_n3")
            .original_key("mixed_n3")
            .build(NodeKind::Dispatch)
            .unwrap();

        let skel = SkeletonBuilder::default()
            .nodes(vec![node0, node1, node2, node3])
            .root_id(Some(0))
            .build()
            .unwrap();

        let got = skel.measure_tree_leaf_granularity().unwrap();
        info!("Measured leaf granularity = {}", got);
        assert!((got - 0.25).abs() < 1e-6, "Expected 0.25, got={}", got);
    }
}