Skip to main content

perspt_sdk/search/
select.rs

1//! Deterministic selection and frontier ordering (PSP-10 Proposition 5,
2//! system 19).
3//!
4//! No model confidence or vote appears in either rule. Every float
5//! comparison uses `total_cmp`; the final key is the unique branch id, so
6//! the order is total and the selection unique.
7
8use serde::{Deserialize, Serialize};
9
10use super::domain_types::BranchMeasurement;
11
12/// One eligible candidate offered to the selection rule.
13#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
14pub struct BranchCandidate {
15    pub measurement: BranchMeasurement,
16    /// Measured improvement in the targeted residual cluster (selection
17    /// key 3); higher is better.
18    pub targeted_improvement: f64,
19}
20
21/// Proposition 5: hard pass, lower normalized energy, greater targeted
22/// improvement, lower recorded cost, lexicographically smaller branch id.
23/// Candidates whose sensor profile differs from the (unique) profile of
24/// the eligible set are excluded before comparison — a profile mismatch is
25/// never normalized after the fact.
26pub fn select_branch(eligible: &[BranchCandidate]) -> Option<&BranchCandidate> {
27    let profile = &eligible.first()?.measurement.sensor_profile;
28    let comparable: Vec<&BranchCandidate> = eligible
29        .iter()
30        .filter(|candidate| candidate.measurement.sensor_profile == *profile)
31        .collect();
32    comparable.into_iter().min_by(|a, b| {
33        b.measurement
34            .hard_pass
35            .cmp(&a.measurement.hard_pass)
36            .then_with(|| a.measurement.energy.total_cmp(&b.measurement.energy))
37            .then_with(|| b.targeted_improvement.total_cmp(&a.targeted_improvement))
38            .then_with(|| a.measurement.cost.total_cmp(&b.measurement.cost))
39            .then_with(|| a.measurement.branch_id.cmp(&b.measurement.branch_id))
40    })
41}
42
43/// One live frontier entry awaiting its quantum (system 19).
44#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
45pub struct FrontierEntry {
46    pub branch_id: String,
47    /// Whether an operational barrier currently holds against this branch.
48    pub barrier_blocked: bool,
49    pub unresolved_obligations: u32,
50    /// Dominant residual magnitude under the common sensor profile.
51    pub dominant_residual: f64,
52    pub cumulative_cost: f64,
53    /// Partial-checkpoint chain depth from the accepted root.
54    pub checkpoint_depth: u32,
55}
56
57/// The deterministic epoch scheduling key (system 19's six-key order).
58/// A heuristic for fairness — not an energy function, an acceptance rule,
59/// or a safety certificate.
60pub fn frontier_order(a: &FrontierEntry, b: &FrontierEntry) -> std::cmp::Ordering {
61    a.barrier_blocked
62        .cmp(&b.barrier_blocked)
63        .then_with(|| a.unresolved_obligations.cmp(&b.unresolved_obligations))
64        .then_with(|| a.dominant_residual.total_cmp(&b.dominant_residual))
65        .then_with(|| a.cumulative_cost.total_cmp(&b.cumulative_cost))
66        .then_with(|| a.checkpoint_depth.cmp(&b.checkpoint_depth))
67        .then_with(|| a.branch_id.cmp(&b.branch_id))
68}
69
70#[cfg(test)]
71mod tests {
72    use super::*;
73
74    fn candidate(
75        branch: &str,
76        hard: bool,
77        energy: f64,
78        improve: f64,
79        cost: f64,
80    ) -> BranchCandidate {
81        BranchCandidate {
82            measurement: BranchMeasurement {
83                branch_id: branch.into(),
84                candidate_id: format!("{branch}/c1"),
85                energy,
86                hard_pass: hard,
87                residuals: vec![],
88                sensor_profile: "profile-1".into(),
89                cost,
90            },
91            targeted_improvement: improve,
92        }
93    }
94
95    fn winner(candidates: Vec<BranchCandidate>) -> String {
96        select_branch(&candidates)
97            .unwrap()
98            .measurement
99            .branch_id
100            .clone()
101    }
102
103    #[test]
104    fn the_five_keys_apply_in_order() {
105        // Key 1: hard pass beats lower energy.
106        assert_eq!(
107            winner(vec![
108                candidate("a", false, 0.1, 9.0, 0.0),
109                candidate("b", true, 5.0, 0.0, 9.0),
110            ]),
111            "b"
112        );
113        // Key 2: lower energy.
114        assert_eq!(
115            winner(vec![
116                candidate("a", false, 2.0, 9.0, 0.0),
117                candidate("b", false, 1.0, 0.0, 9.0),
118            ]),
119            "b"
120        );
121        // Key 3: greater targeted improvement at equal energy.
122        assert_eq!(
123            winner(vec![
124                candidate("a", false, 1.0, 0.5, 0.0),
125                candidate("b", false, 1.0, 0.9, 9.0),
126            ]),
127            "b"
128        );
129        // Key 4: lower cost.
130        assert_eq!(
131            winner(vec![
132                candidate("a", false, 1.0, 0.5, 3.0),
133                candidate("b", false, 1.0, 0.5, 2.0),
134            ]),
135            "b"
136        );
137        // Key 5: branch id breaks every remaining tie.
138        assert_eq!(
139            winner(vec![
140                candidate("b", false, 1.0, 0.5, 2.0),
141                candidate("a", false, 1.0, 0.5, 2.0),
142            ]),
143            "a"
144        );
145    }
146
147    #[test]
148    fn profile_mismatches_are_excluded_not_normalized() {
149        let mut other = candidate("z", false, 0.0, 9.0, 0.0);
150        other.measurement.sensor_profile = "profile-2".into();
151        let candidates = vec![candidate("a", false, 1.0, 0.0, 0.0), other];
152        let picked = select_branch(&candidates).unwrap();
153        assert_eq!(picked.measurement.branch_id, "a");
154    }
155
156    #[test]
157    fn frontier_order_is_total_and_fair() {
158        let entry = |id: &str, blocked, obligations, residual, cost, depth| FrontierEntry {
159            branch_id: id.into(),
160            barrier_blocked: blocked,
161            unresolved_obligations: obligations,
162            dominant_residual: residual,
163            cumulative_cost: cost,
164            checkpoint_depth: depth,
165        };
166        let mut entries = [
167            entry("c", false, 1, 2.0, 1.0, 1),
168            entry("a", true, 0, 0.0, 0.0, 0),
169            entry("b", false, 1, 2.0, 1.0, 1),
170            entry("d", false, 0, 5.0, 9.0, 3),
171        ];
172        entries.sort_by(frontier_order);
173        let order: Vec<&str> = entries.iter().map(|e| e.branch_id.as_str()).collect();
174        // Unblocked first; fewer obligations; then residual/cost/depth/id.
175        assert_eq!(order, ["d", "b", "c", "a"]);
176    }
177}