Skip to main content

aft/commands/semantic_search/
paging.rs

1use std::fmt;
2use std::ops::Range;
3
4use serde_json::Value;
5
6use super::blocks::{
7    BlockBuildError, BlockBuilder, BlockReply, PageRequest, MAX_BLOCK_DEPTH, MAX_OFFSET,
8    MAX_PUBLIC_TOP_K,
9};
10use super::blocks::{StabilityUnit, BLOCK_DEPTHS};
11
12pub const DEFAULT_TOP_K: usize = 10;
13
14#[derive(Debug, Clone, Copy, PartialEq, Eq)]
15pub struct ValidatedPageRequest {
16    offset: usize,
17    top_k: usize,
18}
19
20impl ValidatedPageRequest {
21    pub fn offset(self) -> usize {
22        self.offset
23    }
24
25    pub fn top_k(self) -> usize {
26        self.top_k
27    }
28
29    pub fn interval_end(self) -> u64 {
30        (self.offset as u64).saturating_add(self.top_k as u64)
31    }
32
33    fn as_block_request(self) -> PageRequest {
34        PageRequest {
35            offset: self.offset,
36            top_k: self.top_k,
37        }
38    }
39}
40
41#[derive(Debug, Clone, PartialEq, Eq)]
42pub struct PagingValidationError {
43    field: &'static str,
44    message: String,
45}
46
47impl PagingValidationError {
48    pub const fn code(&self) -> &'static str {
49        "invalid_request"
50    }
51
52    pub const fn field(&self) -> &'static str {
53        self.field
54    }
55}
56
57impl fmt::Display for PagingValidationError {
58    fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
59        formatter.write_str(&self.message)
60    }
61}
62
63impl std::error::Error for PagingValidationError {}
64
65#[derive(Debug)]
66pub enum PagingError {
67    InvalidRequest(PagingValidationError),
68    Build(BlockBuildError),
69}
70
71impl fmt::Display for PagingError {
72    fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
73        match self {
74            Self::InvalidRequest(error) => error.fmt(formatter),
75            Self::Build(error) => error.fmt(formatter),
76        }
77    }
78}
79
80impl std::error::Error for PagingError {}
81
82impl From<PagingValidationError> for PagingError {
83    fn from(error: PagingValidationError) -> Self {
84        Self::InvalidRequest(error)
85    }
86}
87
88impl From<BlockBuildError> for PagingError {
89    fn from(error: BlockBuildError) -> Self {
90        Self::Build(error)
91    }
92}
93
94fn invalid(field: &'static str, message: impl Into<String>) -> PagingValidationError {
95    PagingValidationError {
96        field,
97        message: message.into(),
98    }
99}
100
101fn parse_integer(
102    value: Option<&Value>,
103    field: &'static str,
104    default: usize,
105    minimum: usize,
106    maximum: usize,
107    maximum_name: &'static str,
108) -> Result<usize, PagingValidationError> {
109    let Some(value) = value else {
110        return Ok(default);
111    };
112    let Some(integer) = value.as_i64() else {
113        return Err(invalid(
114            field,
115            format!("{field} must be an integer between {minimum} and {maximum_name} ({maximum})"),
116        ));
117    };
118    if integer < minimum as i64 || integer > maximum as i64 {
119        return Err(invalid(
120            field,
121            format!("{field} must be between {minimum} and {maximum_name} ({maximum})"),
122        ));
123    }
124    Ok(integer as usize)
125}
126
127/// Validates both public bounds before constructing a request that can compute an interval.
128pub fn parse_public_page_request(
129    params: &Value,
130) -> Result<ValidatedPageRequest, PagingValidationError> {
131    let offset = parse_integer(
132        params.get("offset"),
133        "offset",
134        0,
135        0,
136        MAX_OFFSET,
137        "MAX_OFFSET",
138    )?;
139    let top_k_value = match (params.get("topK"), params.get("top_k")) {
140        (Some(_), Some(_)) => {
141            return Err(invalid("topK", "topK and top_k cannot both be supplied"));
142        }
143        (Some(value), None) | (None, Some(value)) => Some(value),
144        (None, None) => None,
145    };
146    let top_k = parse_integer(
147        top_k_value,
148        "topK",
149        DEFAULT_TOP_K,
150        1,
151        MAX_PUBLIC_TOP_K,
152        "MAX_TOP_K",
153    )?;
154    Ok(ValidatedPageRequest { offset, top_k })
155}
156
157#[derive(Debug, Clone, Copy, PartialEq, Eq)]
158pub enum StopState {
159    S1MoreAtDepth,
160    S2Exhausted,
161    S3DepthCap,
162}
163
164#[derive(Debug, Clone, Copy, PartialEq, Eq)]
165pub struct StopConditions {
166    pub interval_satisfied: bool,
167    pub lanes_exhausted: bool,
168    pub at_depth_cap: bool,
169}
170
171#[derive(Debug, Clone, Copy, PartialEq, Eq)]
172pub struct UnreachableLoopExit;
173
174impl fmt::Display for UnreachableLoopExit {
175    fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
176        formatter.write_str(
177            "the escalation loop cannot exit below the depth cap while its interval is unsatisfied and candidates remain",
178        )
179    }
180}
181
182impl std::error::Error for UnreachableLoopExit {}
183
184/// When conditions coincide, selects one deterministic state by prioritizing
185/// exhaustion, then the depth cap, then interval satisfaction.
186pub fn select_stop_state(conditions: StopConditions) -> Result<StopState, UnreachableLoopExit> {
187    if conditions.lanes_exhausted {
188        return Ok(StopState::S2Exhausted);
189    }
190    if conditions.at_depth_cap {
191        return Ok(StopState::S3DepthCap);
192    }
193    if conditions.interval_satisfied {
194        return Ok(StopState::S1MoreAtDepth);
195    }
196    Err(UnreachableLoopExit)
197}
198
199#[derive(Debug, Clone, PartialEq)]
200pub struct SearchPage {
201    pub reply: BlockReply,
202    pub stop_state: StopState,
203    pub stability_void: bool,
204}
205
206impl SearchPage {
207    pub fn shown(&self) -> usize {
208        self.reply.page.len()
209    }
210
211    pub fn total_at_stop(&self) -> usize {
212        self.reply.canonical_list.len()
213    }
214}
215
216pub fn serve_public_page(
217    builder: &BlockBuilder,
218    request: ValidatedPageRequest,
219) -> Result<SearchPage, PagingError> {
220    let interval_end = request.interval_end();
221    let reply = builder.build_for_request(request.as_block_request())?;
222    let stop_state = select_stop_state(StopConditions {
223        interval_satisfied: (reply.canonical_list.len() as u64) >= interval_end,
224        lanes_exhausted: reply.lanes_exhausted,
225        at_depth_cap: reply.retrieval_depth == MAX_BLOCK_DEPTH,
226    })
227    .expect("BlockBuilder can only return at a valid escalation-loop exit");
228    Ok(SearchPage {
229        reply,
230        stop_state,
231        stability_void: false,
232    })
233}
234
235/// Metadata describing a canonical prefix and the depth at which it became stable.
236#[derive(Debug, Clone, PartialEq)]
237pub struct ReferenceList {
238    pub stability_units: Vec<StabilityUnit>,
239    pub retrieval_depth: usize,
240    pub depth_tier: usize,
241    pub lanes_exhausted: bool,
242}
243
244/// Builds a canonical prefix for comparison checks; unlike public requests,
245/// this helper may process an interval containing more than MAX_TOP_K items.
246pub(crate) fn build_l(
247    builder: &BlockBuilder,
248    interval: Range<u64>,
249) -> Result<ReferenceList, BlockBuildError> {
250    assert_eq!(interval.start, 0, "build_l accepts only canonical prefixes");
251    let target = interval.end;
252    let mut tier = BLOCK_DEPTHS
253        .iter()
254        .position(|depth| (*depth as u64) >= target)
255        .unwrap_or(BLOCK_DEPTHS.len() - 1);
256
257    loop {
258        let reply = builder.build_at_depth(BLOCK_DEPTHS[tier])?;
259        let enough = (reply.canonical_list.len() as u64) >= target;
260        if enough || reply.lanes_exhausted || tier + 1 == BLOCK_DEPTHS.len() {
261            return Ok(ReferenceList {
262                stability_units: reply
263                    .canonical_list
264                    .stability_units()
265                    .into_iter()
266                    .take(target.min(usize::MAX as u64) as usize)
267                    .collect(),
268                retrieval_depth: reply.retrieval_depth,
269                depth_tier: reply.depth_tier,
270                lanes_exhausted: reply.lanes_exhausted,
271            });
272        }
273        tier += 1;
274    }
275}
276
277/// Builds canonical prefix metadata without applying public request-size bounds.
278pub fn build_reference_list(
279    builder: &BlockBuilder,
280    interval: Range<u64>,
281) -> Result<ReferenceList, BlockBuildError> {
282    build_l(builder, interval)
283}