Skip to main content

strop_engine/editor/picker/
ranking.rs

1//! Correlated picker ranking: native CPU actors publish through the same AppEvent
2//! and replay boundary as every other service. No scoring occurs on an input key.
3use super::{Editor, PickerId};
4use std::collections::HashSet;
5use std::sync::mpsc::{self, Receiver, Sender};
6use strop_core::worker::{FailureKind, Outcome, Ticket};
7use strop_picker::{RankingEvent, RankingWorker};
8
9#[derive(Debug, Clone, PartialEq, Eq, serde::Serialize, serde::Deserialize)]
10pub struct Key {
11    pub picker: PickerId,
12    pub query: String,
13    pub items: usize,
14}
15#[derive(serde::Serialize, serde::Deserialize)]
16pub struct Event {
17    pub picker: PickerId,
18    pub update: RankingEvent<Key>,
19}
20pub(crate) struct State {
21    pub tx: Sender<Event>,
22    pub rx: Option<Receiver<Event>>,
23    pub retiring: HashSet<PickerId>,
24}
25impl Default for State {
26    fn default() -> Self {
27        let (tx, rx) = mpsc::channel();
28        Self {
29            tx,
30            rx: Some(rx),
31            retiring: HashSet::new(),
32        }
33    }
34}
35impl Editor {
36    pub(super) fn start_picker_ranking(&mut self) {
37        let Some(glue) = self.picker.as_mut() else {
38            return;
39        };
40        let id = glue.id;
41        glue.rank_alive = true;
42        match self.tape.request("picker.ranker", &id) {
43            Ok(false) => {
44                self.request_picker_ranking();
45                return;
46            }
47            Err(error) => {
48                glue.rank_alive = false;
49                glue.picker.error = Some(error.to_string());
50                return;
51            }
52            Ok(true) => {}
53        }
54        let tx = self.picker_ranking.tx.clone();
55        match RankingWorker::start(move |update| tx.send(Event { picker: id, update }).is_ok()) {
56            Ok(worker) => glue.rank_worker = Some(worker),
57            Err(error) => {
58                glue.rank_alive = false;
59                glue.picker.error = Some(format!("picker ranking unavailable: {error}"));
60                return;
61            }
62        }
63        self.request_picker_ranking();
64    }
65
66    pub(crate) fn request_picker_ranking(&mut self) {
67        let Some(glue) = self.picker.as_mut() else {
68            return;
69        };
70        if !glue.rank_alive {
71            return;
72        }
73        // staleness compares against the EFFECTIVE rank query, like the
74        // install check below (0051: qualifiers never rank — `language:rust`
75        // edits must not clear results while the needle is unchanged)
76        let effective = glue
77            .picker
78            .rank_query
79            .clone()
80            .unwrap_or_else(|| glue.picker.input.text.clone());
81        if glue.ranked_query.as_deref() != Some(effective.as_str()) {
82            glue.picker.clear_results();
83        }
84        if glue.rank_pending.is_some() {
85            if !glue.rank_dirty {
86                if let Some(worker) = &glue.rank_worker {
87                    worker.cancel_pending();
88                }
89            }
90            glue.rank_dirty = true;
91            return;
92        }
93        glue.rank_dirty = false;
94        let request = match self.worker_ids.allocate() {
95            Ok(request) => request,
96            Err(error) => {
97                glue.picker.error = Some(error.message);
98                return;
99            }
100        };
101        let filter = glue.picker.filter_request();
102        let ticket = Ticket {
103            request,
104            key: Key {
105                picker: glue.id,
106                query: filter.query.clone(),
107                items: filter.catalog.len(),
108            },
109        };
110        glue.rank_pending = Some(ticket.clone());
111        match self.tape.request("picker.rank", &ticket) {
112            Ok(false) => return,
113            Ok(true) => {}
114            Err(error) => {
115                self.handle_picker_ranking(Event {
116                    picker: ticket.key.picker,
117                    update: RankingEvent::Completed(strop_core::worker::Completion {
118                        ticket,
119                        outcome: Outcome::failed(FailureKind::Protocol, error.to_string()),
120                    }),
121                });
122                return;
123            }
124        }
125        let result = glue
126            .rank_worker
127            .as_ref()
128            .ok_or_else(|| std::io::Error::other("picker ranking worker is absent"))
129            .and_then(|worker| worker.submit(ticket.clone(), filter));
130        if let Err(error) = result {
131            self.handle_picker_ranking(Event {
132                picker: ticket.key.picker,
133                update: RankingEvent::Completed(strop_core::worker::Completion {
134                    ticket,
135                    outcome: Outcome::failed(FailureKind::Disconnected, error.to_string()),
136                }),
137            });
138        }
139    }
140
141    pub(crate) fn handle_picker_ranking(&mut self, event: Event) {
142        if matches!(&event.update, RankingEvent::Stopped) {
143            self.picker_ranking.retiring.remove(&event.picker);
144        }
145        let Some(glue) = self.picker.as_mut().filter(|glue| glue.id == event.picker) else {
146            return;
147        };
148        match event.update {
149            RankingEvent::Completed(completion) => {
150                if glue.rank_pending.as_ref() != Some(&completion.ticket) {
151                    return;
152                }
153                glue.rank_pending = None;
154                if std::mem::take(&mut glue.rank_dirty) {
155                    self.request_picker_ranking();
156                    return;
157                }
158                match completion.outcome {
159                    Outcome::Success(ranking) => {
160                        // staleness compares against the EFFECTIVE rank
161                        // query (0051: qualifiers never rank — the needle
162                        // may differ from the raw input)
163                        let effective = glue
164                            .picker
165                            .rank_query
166                            .clone()
167                            .unwrap_or_else(|| glue.picker.input.text.clone());
168                        if completion.ticket.key.query != effective
169                            || !glue.picker.install_ranking(ranking)
170                        {
171                            return;
172                        }
173                        glue.ranked_query = Some(completion.ticket.key.query);
174                    }
175                    Outcome::Failed { failure, .. } => {
176                        if failure.kind == strop_core::worker::FailureKind::Protocol {
177                            if let Some(query) = &glue.query {
178                                glue.query_highlights
179                                    .push(strop_picker::query::HighlightSpan {
180                                        range: query.content_range(),
181                                        role: strop_picker::query::Role::Error,
182                                    });
183                            }
184                        }
185                        glue.picker.error = Some(failure.message);
186                        glue.accept_when_ranked = false;
187                    }
188                    Outcome::Cancelled(_) => glue.accept_when_ranked = false,
189                }
190            }
191            RankingEvent::Stopped => {
192                glue.rank_alive = false;
193                if glue.rank_pending.take().is_some() {
194                    glue.picker.error = Some("picker ranking worker stopped".into());
195                    glue.revoke(strop_core::worker::CancelReason::OwnerClosed);
196                    glue.picker.streaming = false;
197                }
198            }
199        }
200        self.finish_pending_picker_accept();
201    }
202}