|
| 1 | +//! # `head2head` — competing-expert superposition + winner selection. |
| 2 | +//! |
| 3 | +//! Two (or N) mailbox-experts reason over the same input *in parallel*, each |
| 4 | +//! posting a [`BlackboardEntry`]; head2head selects **one** winner whose |
| 5 | +//! emissions become the authoritative spiral. This is the *selection* half of |
| 6 | +//! the superposition (item: "head2head mailbox thinking as superposition") — the |
| 7 | +//! parallel mailbox *execution* is the CI-gated consumer side; the |
| 8 | +//! winner-pick over the blackboard is what lives, zero-dep and verifiable, here. |
| 9 | +//! |
| 10 | +//! ## The Go metaphor (the user's framing) |
| 11 | +//! Two strategies compete and the board scores them: |
| 12 | +//! - **infight** (close tactical combat) ≈ [`WinnerCriterion::DissonanceMin`]: |
| 13 | +//! the expert with the least internal contradiction — tightest local resolution. |
| 14 | +//! - **Raumgewinn** (territory / influence) ≈ [`WinnerCriterion::SupportSpread`]: |
| 15 | +//! the expert whose top-K support atoms cover the most distinct ground. |
| 16 | +//! |
| 17 | +//! ## Separation of concerns — select, never duplicate |
| 18 | +//! [`Head2Head::select`] is a pure read + arg-extremum over the *existing* |
| 19 | +//! [`Blackboard`] entries (`confidence` / `dissonance` / `support` are already |
| 20 | +//! there). It stores no new identity and copies nothing — an identity is |
| 21 | +//! *pointed at* ([`ExpertId`]), never re-materialized (`I-VSA-IDENTITIES`). The |
| 22 | +//! winner is a *decision over state*, not a new copy of it. |
| 23 | +//! |
| 24 | +//! Zero dependencies. Pure data + one method on the selector carrier. |
| 25 | +
|
| 26 | +// Pedantic carve-outs (the repo does not deny pedantic; these are in-context false |
| 27 | +// positives): cast_precision_loss — the support count n ∈ 0..=4 is exact as f32; |
| 28 | +// float_cmp — tests compare exact integer-valued scores; missing_const_for_fn — |
| 29 | +// `score` transitively calls the non-const `slice::contains`. |
| 30 | +#![allow(clippy::cast_precision_loss, clippy::float_cmp, clippy::missing_const_for_fn)] |
| 31 | + |
| 32 | +use crate::a2a_blackboard::{Blackboard, BlackboardEntry, ExpertId}; |
| 33 | + |
| 34 | +/// How to pick the winner among competing experts. Each maps a |
| 35 | +/// [`BlackboardEntry`] to a scalar score; the highest score wins. |
| 36 | +#[derive(Clone, Copy, Debug, PartialEq, Eq, Default)] |
| 37 | +pub enum WinnerCriterion { |
| 38 | + /// Lowest dissonance wins — the **infight** reading: least internal |
| 39 | + /// contradiction (score = `1 - dissonance`). |
| 40 | + DissonanceMin, |
| 41 | + /// Highest self-reported confidence wins (score = `confidence`). |
| 42 | + ConfidenceMax, |
| 43 | + /// Widest distinct support wins — the **Raumgewinn**/territory reading |
| 44 | + /// (score = count of distinct non-zero atoms in `support[4]`). |
| 45 | + SupportSpread, |
| 46 | + /// Confidence tempered by dissonance: `confidence * (1 - dissonance)`. |
| 47 | + /// The default — rewards certainty that isn't bought with contradiction. |
| 48 | + #[default] |
| 49 | + Tempered, |
| 50 | +} |
| 51 | + |
| 52 | +/// The outcome of a head2head: who won, by how much, under which criterion. |
| 53 | +/// |
| 54 | +/// `margin` is the winner's lead over the best *other* expert |
| 55 | +/// (`winner_score - runner_up_score`); with no distinct runner-up it is the |
| 56 | +/// winner's uncontested score (lead over the `0.0` floor). A small `margin` is |
| 57 | +/// the dark-horse signal — the wave was nearly a coin-flip, so the deterministic |
| 58 | +/// particle chain should confirm before the winner's spiral is trusted. |
| 59 | +#[derive(Clone, Copy, Debug, PartialEq)] |
| 60 | +pub struct CompetitionOutcome { |
| 61 | + /// The expert whose top-scoring bid won. |
| 62 | + pub winner: ExpertId, |
| 63 | + /// The winner's score under `criterion`. |
| 64 | + pub winner_score: f32, |
| 65 | + /// The best *other* expert, if any competed. |
| 66 | + pub runner_up: Option<ExpertId>, |
| 67 | + /// `winner_score - runner_up_score` (lead over the `0.0` floor if uncontested). |
| 68 | + pub margin: f32, |
| 69 | + /// The criterion the board judged by. |
| 70 | + pub criterion: WinnerCriterion, |
| 71 | +} |
| 72 | + |
| 73 | +/// The board judge: holds the [`WinnerCriterion`] and scores competitors. |
| 74 | +#[derive(Clone, Copy, Debug, Default, PartialEq, Eq)] |
| 75 | +pub struct Head2Head { |
| 76 | + /// How this judge scores each bid. |
| 77 | + pub criterion: WinnerCriterion, |
| 78 | +} |
| 79 | + |
| 80 | +impl Head2Head { |
| 81 | + /// A judge with an explicit criterion. |
| 82 | + #[must_use] |
| 83 | + pub fn new(criterion: WinnerCriterion) -> Self { |
| 84 | + Self { criterion } |
| 85 | + } |
| 86 | + |
| 87 | + /// Score one bid under this judge's criterion (higher = better). |
| 88 | + #[must_use] |
| 89 | + pub fn score(&self, e: &BlackboardEntry) -> f32 { |
| 90 | + match self.criterion { |
| 91 | + WinnerCriterion::DissonanceMin => 1.0 - e.dissonance, |
| 92 | + WinnerCriterion::ConfidenceMax => e.confidence, |
| 93 | + WinnerCriterion::SupportSpread => distinct_support(e.support) as f32, |
| 94 | + WinnerCriterion::Tempered => e.confidence * (1.0 - e.dissonance), |
| 95 | + } |
| 96 | + } |
| 97 | + |
| 98 | + /// Select the winning expert over the blackboard's bids, or `None` if the |
| 99 | + /// board is empty. Each entry is a *bid*; the expert of the top-scoring bid |
| 100 | + /// wins, and the runner-up is the top bid from any *other* expert. |
| 101 | + #[must_use] |
| 102 | + pub fn select(&self, bb: &Blackboard) -> Option<CompetitionOutcome> { |
| 103 | + let best = bb |
| 104 | + .entries |
| 105 | + .iter() |
| 106 | + .max_by(|a, b| self.score(a).total_cmp(&self.score(b)))?; |
| 107 | + let winner = best.expert_id; |
| 108 | + let winner_score = self.score(best); |
| 109 | + |
| 110 | + let runner = bb |
| 111 | + .entries |
| 112 | + .iter() |
| 113 | + .filter(|e| e.expert_id != winner) |
| 114 | + .max_by(|a, b| self.score(a).total_cmp(&self.score(b))); |
| 115 | + let (runner_up, runner_score) = |
| 116 | + runner.map_or((None, 0.0), |r| (Some(r.expert_id), self.score(r))); |
| 117 | + |
| 118 | + Some(CompetitionOutcome { |
| 119 | + winner, |
| 120 | + winner_score, |
| 121 | + runner_up, |
| 122 | + margin: winner_score - runner_score, |
| 123 | + criterion: self.criterion, |
| 124 | + }) |
| 125 | + } |
| 126 | +} |
| 127 | + |
| 128 | +/// Count of distinct non-zero atoms in a 4-slot support vector (the territory |
| 129 | +/// measure for [`WinnerCriterion::SupportSpread`]). `0` is the no-atom sentinel. |
| 130 | +fn distinct_support(support: [u16; 4]) -> usize { |
| 131 | + let mut seen = [0u16; 4]; |
| 132 | + let mut n = 0; |
| 133 | + for a in support { |
| 134 | + if a != 0 && !seen[..n].contains(&a) { |
| 135 | + seen[n] = a; |
| 136 | + n += 1; |
| 137 | + } |
| 138 | + } |
| 139 | + n |
| 140 | +} |
| 141 | + |
| 142 | +#[cfg(test)] |
| 143 | +mod tests { |
| 144 | + use super::*; |
| 145 | + use crate::a2a_blackboard::ExpertCapability; |
| 146 | + |
| 147 | + fn entry(id: ExpertId, confidence: f32, dissonance: f32, support: [u16; 4]) -> BlackboardEntry { |
| 148 | + BlackboardEntry { |
| 149 | + expert_id: id, |
| 150 | + capability: ExpertCapability::ReasoningTopology, |
| 151 | + result: 0, |
| 152 | + confidence, |
| 153 | + support, |
| 154 | + dissonance, |
| 155 | + cost_us: 0, |
| 156 | + } |
| 157 | + } |
| 158 | + |
| 159 | + fn board(es: Vec<BlackboardEntry>) -> Blackboard { |
| 160 | + Blackboard { entries: es, round: 0 } |
| 161 | + } |
| 162 | + |
| 163 | + #[test] |
| 164 | + fn tempered_default_rewards_certainty_without_contradiction() { |
| 165 | + // A: high confidence but high dissonance (0.9 * 0.4 = 0.36). |
| 166 | + // B: lower confidence but clean (0.7 * 0.9 = 0.63 wins). |
| 167 | + let bb = board(vec![ |
| 168 | + entry(1, 0.9, 0.6, [0; 4]), |
| 169 | + entry(2, 0.7, 0.1, [0; 4]), |
| 170 | + ]); |
| 171 | + let out = Head2Head::default().select(&bb).unwrap(); |
| 172 | + assert_eq!(out.criterion, WinnerCriterion::Tempered); |
| 173 | + assert_eq!(out.winner, 2); |
| 174 | + assert_eq!(out.runner_up, Some(1)); |
| 175 | + assert!(out.margin > 0.0); |
| 176 | + } |
| 177 | + |
| 178 | + #[test] |
| 179 | + fn dissonance_min_is_the_infight_pick() { |
| 180 | + // Same confidence; the tighter (lower-dissonance) expert wins the infight. |
| 181 | + let bb = board(vec![ |
| 182 | + entry(1, 0.8, 0.5, [0; 4]), |
| 183 | + entry(2, 0.8, 0.2, [0; 4]), |
| 184 | + ]); |
| 185 | + let out = Head2Head::new(WinnerCriterion::DissonanceMin).select(&bb).unwrap(); |
| 186 | + assert_eq!(out.winner, 2); |
| 187 | + } |
| 188 | + |
| 189 | + #[test] |
| 190 | + fn support_spread_is_the_raumgewinn_pick() { |
| 191 | + // Same confidence/dissonance; the expert covering more distinct ground wins. |
| 192 | + let bb = board(vec![ |
| 193 | + entry(1, 0.8, 0.1, [42, 42, 0, 0]), // 1 distinct atom |
| 194 | + entry(2, 0.8, 0.1, [7, 9, 13, 21]), // 4 distinct atoms (territory) |
| 195 | + ]); |
| 196 | + let out = Head2Head::new(WinnerCriterion::SupportSpread).select(&bb).unwrap(); |
| 197 | + assert_eq!(out.winner, 2); |
| 198 | + assert_eq!(out.winner_score, 4.0); |
| 199 | + assert_eq!(out.margin, 3.0); // 4 distinct vs 1 distinct |
| 200 | + } |
| 201 | + |
| 202 | + #[test] |
| 203 | + fn confidence_max_ignores_dissonance() { |
| 204 | + let bb = board(vec![ |
| 205 | + entry(1, 0.95, 0.9, [0; 4]), // noisy but loud → wins on raw confidence |
| 206 | + entry(2, 0.6, 0.0, [0; 4]), |
| 207 | + ]); |
| 208 | + let out = Head2Head::new(WinnerCriterion::ConfidenceMax).select(&bb).unwrap(); |
| 209 | + assert_eq!(out.winner, 1); |
| 210 | + } |
| 211 | + |
| 212 | + #[test] |
| 213 | + fn uncontested_single_expert_has_no_runner_up() { |
| 214 | + let bb = board(vec![entry(7, 0.8, 0.2, [0; 4])]); |
| 215 | + let out = Head2Head::default().select(&bb).unwrap(); |
| 216 | + assert_eq!(out.winner, 7); |
| 217 | + assert_eq!(out.runner_up, None); |
| 218 | + assert_eq!(out.margin, out.winner_score); // lead over the 0.0 floor |
| 219 | + } |
| 220 | + |
| 221 | + #[test] |
| 222 | + fn multiple_bids_per_expert_use_the_experts_best() { |
| 223 | + // Expert 1 posts twice; its best bid represents it; runner-up is expert 2. |
| 224 | + let bb = board(vec![ |
| 225 | + entry(1, 0.3, 0.0, [0; 4]), |
| 226 | + entry(1, 0.9, 0.0, [0; 4]), // expert 1's best |
| 227 | + entry(2, 0.5, 0.0, [0; 4]), |
| 228 | + ]); |
| 229 | + let out = Head2Head::new(WinnerCriterion::ConfidenceMax).select(&bb).unwrap(); |
| 230 | + assert_eq!(out.winner, 1); |
| 231 | + assert_eq!(out.runner_up, Some(2)); |
| 232 | + } |
| 233 | + |
| 234 | + #[test] |
| 235 | + fn empty_board_has_no_winner() { |
| 236 | + assert!(Head2Head::default().select(&board(vec![])).is_none()); |
| 237 | + } |
| 238 | +} |
0 commit comments