Skip to main content

rustev_core/ranking/
select.rs

1//! Pure selection and exact backfill (039, 3.10-3.11).
2use rustev_contract::judgment::{Derivation, Lineage, Unresolved};
3use rustev_contract::ranking::{BackendFailure, Backfill, Select};
4use rustev_contract::ranking_result::*;
5use std::collections::BTreeSet;
6
7/// A resolved source's entries in source order, or its unresolved reason.
8/// Later evaluation adapts semantic and composed rankings to this same seam.
9pub struct Source<'a> {
10    pub ids: Result<&'a [String], Unresolved>,
11    pub partial: bool,
12    pub derivation: Derivation,
13    pub lineage: &'a Lineage,
14}
15
16/// The caller resolves the declared step reference; both classes are checked.
17pub struct ExactOrdering<'a> {
18    pub from: &'a str,
19    pub static_class: Derivation,
20    pub source: Source<'a>,
21}
22
23#[derive(Debug, Clone, PartialEq, Eq)]
24pub struct SelectionResult {
25    pub value: Result<Selection, Unresolved>,
26    pub derivation: Derivation,
27    pub lineage: Lineage,
28    /// The evaluator emits a fallback notice for its select step with this reason.
29    pub fallback: Option<Unresolved>,
30    /// Retained even when a needed backfill is unresolved.
31    pub backfill: SelectionBackfill,
32}
33
34pub fn backend_failure(reason: &Unresolved) -> bool {
35    matches!(
36        reason,
37        Unresolved::BackendUnavailable { .. }
38            | Unresolved::InvalidBackendOutput { .. }
39            | Unresolved::BudgetExhausted { .. }
40            | Unresolved::DeadlineExceeded
41            | Unresolved::Unsupported { .. }
42    )
43}
44
45fn invalid(detail: &str) -> Unresolved {
46    Unresolved::InvalidInput {
47        fields: vec![],
48        detail: detail.into(),
49    }
50}
51
52/// The pool remains available even when the source failed. Backfill's resolved
53/// value is inspected only when needed, but its declared static class is always
54/// checked. A selection remains mixed-derived even with no model positions.
55pub fn select(
56    declaration: &Select,
57    pool: &[String],
58    model: &Source<'_>,
59    backfill: Option<&ExactOrdering<'_>>,
60) -> SelectionResult {
61    let mut lineage = model.lineage.clone();
62    if let Some(b) = backfill {
63        lineage.inputs.extend(b.source.lineage.inputs.clone());
64        lineage.steps.extend(b.source.lineage.steps.clone());
65        if let Some(producers) = &b.source.lineage.producers {
66            lineage
67                .producers
68                .get_or_insert_with(Default::default)
69                .extend(producers.clone());
70        }
71    }
72    lineage.inputs.sort();
73    lineage.inputs.dedup();
74    lineage.steps.sort();
75    lineage.steps.dedup();
76    let mut result = SelectionResult {
77        value: Err(invalid("selection not evaluated")),
78        derivation: Derivation::MixedDerived,
79        lineage,
80        fallback: None,
81        backfill: SelectionBackfill::None,
82    };
83    result.value = assemble(
84        declaration,
85        pool,
86        model,
87        backfill,
88        &mut result.fallback,
89        &mut result.backfill,
90    );
91    result
92}
93
94fn assemble(
95    d: &Select,
96    pool: &[String],
97    model: &Source<'_>,
98    backfill: Option<&ExactOrdering<'_>>,
99    fallback: &mut Option<Unresolved>,
100    state: &mut SelectionBackfill,
101) -> Result<Selection, Unresolved> {
102    if !(1..=1024).contains(&d.k)
103        || pool.len() > 1024
104        || pool.iter().collect::<BTreeSet<_>>().len() != pool.len()
105    {
106        return Err(invalid("invalid selection bound or pool"));
107    }
108    match (&d.backfill, backfill) {
109        (Backfill::None, None) if d.on_backend_failure != BackendFailure::BackfillAll => {}
110        (
111            Backfill::From {
112                from,
113                exclude_selected: true,
114            },
115            Some(b),
116        ) if from == b.from
117            && b.static_class == Derivation::ExactDerived
118            && b.source.derivation == Derivation::ExactDerived => {}
119        _ => {
120            return Err(invalid(
121                "backfill must match its declared exact-derived ordering",
122            ));
123        }
124    }
125    let mut entries = Vec::new();
126    let mut selected = BTreeSet::new();
127    let status = match &model.ids {
128        Ok(ids) => {
129            // Reject malformed source values, rather than laundering pool escapes
130            // or duplicate positions through the model half of a selection.
131            if ids.len() > pool.len()
132                || ids.iter().any(|id| !pool.contains(id))
133                || ids.iter().collect::<BTreeSet<_>>().len() != ids.len()
134            {
135                return Err(invalid("invalid ranking source ids"));
136            }
137            for (p, id) in ids.iter().take(d.k as usize).enumerate() {
138                selected.insert(id.clone());
139                entries.push(SelectionEntry {
140                    position: p as u64 + 1,
141                    id: id.clone(),
142                    origin: SelectionOrigin::Model,
143                    source_position: p as u64 + 1,
144                    derivation: model.derivation,
145                    lineage: model.lineage.clone(),
146                });
147            }
148            SelectionModel::Ranked {
149                entries: ids.len() as u64,
150                partial: model.partial,
151            }
152        }
153        Err(reason) => {
154            if !backend_failure(reason) || d.on_backend_failure == BackendFailure::Propagate {
155                return Err(reason.clone());
156            }
157            *fallback = Some(reason.clone());
158            SelectionModel::Failed {
159                reason: reason.clone(),
160            }
161        }
162    };
163    if let Some(b) = backfill {
164        if entries.len() as u64 == d.k {
165            *state = SelectionBackfill::NotNeeded;
166        } else {
167            let ids = b.source.ids.as_ref().map_err(|reason| {
168                *state = SelectionBackfill::Unresolved {
169                    reason: reason.clone(),
170                };
171                reason.clone()
172            })?;
173            let (mut taken, mut skipped_selected, mut skipped_outside_pool) = (0, 0, 0);
174            for (p, id) in ids.iter().enumerate() {
175                if entries.len() as u64 == d.k {
176                    break;
177                }
178                if selected.contains(id) {
179                    skipped_selected += 1;
180                } else if !pool.contains(id) {
181                    skipped_outside_pool += 1;
182                } else {
183                    selected.insert(id.clone());
184                    taken += 1;
185                    entries.push(SelectionEntry {
186                        position: entries.len() as u64 + 1,
187                        id: id.clone(),
188                        origin: SelectionOrigin::Backfill,
189                        source_position: p as u64 + 1,
190                        derivation: b.source.derivation,
191                        lineage: b.source.lineage.clone(),
192                    });
193                }
194            }
195            *state = SelectionBackfill::Used {
196                taken,
197                skipped_selected,
198                skipped_outside_pool,
199            };
200        }
201    }
202    Ok(Selection {
203        k: d.k,
204        shortfall: d.k - entries.len() as u64,
205        entries,
206        model: status,
207        backfill: state.clone(),
208    })
209}