Skip to main content

aft/commands/semantic_search/
plan_table.rs

1use std::collections::BTreeMap;
2use std::fmt;
3use std::path::Path;
4
5use serde::{Deserialize, Serialize};
6
7/// Shapes recognized by the search ranking engine.
8#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, PartialOrd, Ord, Serialize, Deserialize)]
9#[serde(rename_all = "snake_case")]
10pub enum SearchShape {
11    Identifier,
12    CodeLiteral,
13    Short,
14    NaturalLanguage,
15    LogExcerpt,
16    Path,
17    Regex,
18}
19
20impl SearchShape {
21    pub const ALL: [SearchShape; 7] = [
22        SearchShape::Identifier,
23        SearchShape::CodeLiteral,
24        SearchShape::Short,
25        SearchShape::NaturalLanguage,
26        SearchShape::LogExcerpt,
27        SearchShape::Path,
28        SearchShape::Regex,
29    ];
30
31    pub fn as_str(&self) -> &'static str {
32        match self {
33            SearchShape::Identifier => "identifier",
34            SearchShape::CodeLiteral => "code_literal",
35            SearchShape::Short => "short",
36            SearchShape::NaturalLanguage => "nl",
37            SearchShape::LogExcerpt => "log_excerpt",
38            SearchShape::Path => "path",
39            SearchShape::Regex => "regex",
40        }
41    }
42
43    pub fn from_str(s: &str) -> Option<Self> {
44        match s {
45            "identifier" => Some(SearchShape::Identifier),
46            "code_literal" => Some(SearchShape::CodeLiteral),
47            "short" | "mixed" => Some(SearchShape::Short),
48            "nl" | "natural_language" => Some(SearchShape::NaturalLanguage),
49            "log_excerpt" | "error_code" => Some(SearchShape::LogExcerpt),
50            "path" => Some(SearchShape::Path),
51            "regex" => Some(SearchShape::Regex),
52            _ => None,
53        }
54    }
55}
56
57impl fmt::Display for SearchShape {
58    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
59        write!(f, "{}", self.as_str())
60    }
61}
62
63/// Lanes participating in search ranking.
64#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, PartialOrd, Ord, Serialize, Deserialize)]
65#[serde(rename_all = "snake_case")]
66pub enum SearchLaneKind {
67    Symbol,
68    Exact,
69    Anchored,
70    Lexical,
71    Variants,
72    Semantic,
73    PathLookup,
74    FallbackWalk,
75    ReadinessDisclosure,
76}
77
78impl SearchLaneKind {
79    pub const ALL: [SearchLaneKind; 9] = [
80        SearchLaneKind::Symbol,
81        SearchLaneKind::Exact,
82        SearchLaneKind::Anchored,
83        SearchLaneKind::Lexical,
84        SearchLaneKind::Variants,
85        SearchLaneKind::Semantic,
86        SearchLaneKind::PathLookup,
87        SearchLaneKind::FallbackWalk,
88        SearchLaneKind::ReadinessDisclosure,
89    ];
90
91    pub fn as_str(&self) -> &'static str {
92        match self {
93            SearchLaneKind::Symbol => "symbol",
94            SearchLaneKind::Exact => "exact",
95            SearchLaneKind::Anchored => "anchored",
96            SearchLaneKind::Lexical => "lexical",
97            SearchLaneKind::Variants => "variants",
98            SearchLaneKind::Semantic => "semantic",
99            SearchLaneKind::PathLookup => "path_lookup",
100            SearchLaneKind::FallbackWalk => "fallback_walk",
101            SearchLaneKind::ReadinessDisclosure => "readiness_disclosure",
102        }
103    }
104
105    pub fn from_str(s: &str) -> Option<Self> {
106        match s {
107            "symbol" => Some(SearchLaneKind::Symbol),
108            "exact" => Some(SearchLaneKind::Exact),
109            "anchored" => Some(SearchLaneKind::Anchored),
110            "lexical" => Some(SearchLaneKind::Lexical),
111            "variants" => Some(SearchLaneKind::Variants),
112            "semantic" => Some(SearchLaneKind::Semantic),
113            "path_lookup" => Some(SearchLaneKind::PathLookup),
114            "fallback_walk" => Some(SearchLaneKind::FallbackWalk),
115            "readiness_disclosure" => Some(SearchLaneKind::ReadinessDisclosure),
116            _ => None,
117        }
118    }
119
120    pub fn default_plan_order_index(&self) -> usize {
121        match self {
122            SearchLaneKind::Symbol => 0,
123            SearchLaneKind::Exact => 1,
124            SearchLaneKind::Anchored => 2,
125            SearchLaneKind::Lexical => 3,
126            SearchLaneKind::Variants => 4,
127            SearchLaneKind::Semantic => 5,
128            SearchLaneKind::PathLookup => 6,
129            SearchLaneKind::FallbackWalk => 7,
130            SearchLaneKind::ReadinessDisclosure => 8,
131        }
132    }
133
134    pub const fn is_scored(self) -> bool {
135        matches!(self, Self::Lexical | Self::Semantic)
136    }
137}
138
139impl fmt::Display for SearchLaneKind {
140    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
141        write!(f, "{}", self.as_str())
142    }
143}
144
145/// A plan entry for a specific (shape, lane) pair.
146#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
147pub struct LanePlanEntry {
148    pub weight: Option<f32>,
149    pub rrf_constant: Option<f32>,
150    pub plan_order_index: usize,
151}
152
153/// Shape-indexed plan table over the complete (shape, lane) cross product.
154#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
155#[serde(transparent)]
156pub struct PlanTable {
157    pub entries: BTreeMap<String, BTreeMap<String, LanePlanEntry>>,
158}
159
160/// Startup or verification error naming the (shape, lane, field) triple.
161#[derive(Debug, Clone, PartialEq, Eq)]
162pub struct PlanTableError {
163    pub shape: String,
164    pub lane: String,
165    pub field: String,
166    pub message: String,
167}
168
169impl fmt::Display for PlanTableError {
170    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
171        write!(
172            f,
173            "plan table error for ({}, {}, {}): {}",
174            self.shape, self.lane, self.field, self.message
175        )
176    }
177}
178
179impl std::error::Error for PlanTableError {}
180
181impl PlanTable {
182    /// Construct the authoritative running plan table from code constants.
183    pub fn running_table() -> Self {
184        let mut entries = BTreeMap::new();
185
186        for shape in SearchShape::ALL {
187            let shape_str = shape.as_str().to_string();
188            let mut lane_map = BTreeMap::new();
189
190            let weights = match shape {
191                SearchShape::Identifier | SearchShape::Short => (0.8f32, 0.2f32),
192                SearchShape::CodeLiteral | SearchShape::LogExcerpt | SearchShape::Path => {
193                    (0.9f32, 0.1f32)
194                }
195                SearchShape::NaturalLanguage => (0.4f32, 0.6f32),
196                SearchShape::Regex => (1.0f32, 0.0f32),
197            };
198
199            for lane in SearchLaneKind::ALL {
200                let weight = match lane {
201                    SearchLaneKind::Lexical => Some(weights.0),
202                    SearchLaneKind::Semantic => Some(weights.1),
203                    _ => None,
204                };
205                lane_map.insert(
206                    lane.as_str().to_string(),
207                    LanePlanEntry {
208                        weight,
209                        rrf_constant: weight.map(|_| 60.0),
210                        plan_order_index: lane.default_plan_order_index(),
211                    },
212                );
213            }
214
215            entries.insert(shape_str, lane_map);
216        }
217
218        Self { entries }
219    }
220
221    /// Load pinned plan table from a JSON string.
222    pub fn from_json(json_str: &str) -> Result<Self, PlanTableError> {
223        serde_json::from_str(json_str).map_err(|e| PlanTableError {
224            shape: "all".to_string(),
225            lane: "all".to_string(),
226            field: "json".to_string(),
227            message: format!("failed to parse plan-table.json: {e}"),
228        })
229    }
230
231    /// Load pinned plan table from a file path.
232    pub fn from_file(path: &Path) -> Result<Self, PlanTableError> {
233        let content = std::fs::read_to_string(path).map_err(|e| PlanTableError {
234            shape: "all".to_string(),
235            lane: "all".to_string(),
236            field: "file".to_string(),
237            message: format!("failed to read {}: {e}", path.display()),
238        })?;
239        Self::from_json(&content)
240    }
241
242    /// Verify this plan table against another (e.g. running table against pinned table).
243    ///
244    /// Asserts:
245    /// - Complete (shape, lane) cross product: missing pair or extra pair is an error.
246    /// - Every exact lane MUST have weight == None and rrf_constant == None.
247    /// - Every weight, rrf_constant, and plan_order_index matches.
248    ///
249    /// Every error names the (shape, lane, field) triple.
250    pub fn verify_against(&self, pinned: &PlanTable) -> Result<(), PlanTableError> {
251        // Check for missing shapes or lanes in pinned relative to self (running)
252        for shape in SearchShape::ALL {
253            let shape_str = shape.as_str();
254            let running_lanes = match self.entries.get(shape_str) {
255                Some(l) => l,
256                None => {
257                    return Err(PlanTableError {
258                        shape: shape_str.to_string(),
259                        lane: "all".to_string(),
260                        field: "pair".to_string(),
261                        message: "shape missing from running table".to_string(),
262                    })
263                }
264            };
265
266            let pinned_lanes = match pinned.entries.get(shape_str) {
267                Some(l) => l,
268                None => {
269                    return Err(PlanTableError {
270                        shape: shape_str.to_string(),
271                        lane: "all".to_string(),
272                        field: "pair".to_string(),
273                        message: "shape missing from pinned table".to_string(),
274                    })
275                }
276            };
277
278            for lane in SearchLaneKind::ALL {
279                let lane_str = lane.as_str();
280                let running_entry = match running_lanes.get(lane_str) {
281                    Some(e) => e,
282                    None => {
283                        return Err(PlanTableError {
284                            shape: shape_str.to_string(),
285                            lane: lane_str.to_string(),
286                            field: "pair".to_string(),
287                            message: "pair missing from running table".to_string(),
288                        })
289                    }
290                };
291
292                let pinned_entry = match pinned_lanes.get(lane_str) {
293                    Some(e) => e,
294                    None => {
295                        return Err(PlanTableError {
296                            shape: shape_str.to_string(),
297                            lane: lane_str.to_string(),
298                            field: "pair".to_string(),
299                            message: "pair missing from pinned table".to_string(),
300                        })
301                    }
302                };
303
304                // Exact lane MUST have null weight and null rrf_constant
305                if lane == SearchLaneKind::Exact {
306                    if pinned_entry.weight.is_some() {
307                        return Err(PlanTableError {
308                            shape: shape_str.to_string(),
309                            lane: lane_str.to_string(),
310                            field: "weight".to_string(),
311                            message: format!(
312                                "exact lane must have null weight, got {:?}",
313                                pinned_entry.weight
314                            ),
315                        });
316                    }
317                    if pinned_entry.rrf_constant.is_some() {
318                        return Err(PlanTableError {
319                            shape: shape_str.to_string(),
320                            lane: lane_str.to_string(),
321                            field: "rrf_constant".to_string(),
322                            message: format!(
323                                "exact lane must have null rrf_constant, got {:?}",
324                                pinned_entry.rrf_constant
325                            ),
326                        });
327                    }
328                }
329
330                // Check weight equality
331                match (running_entry.weight, pinned_entry.weight) {
332                    (Some(w_run), Some(w_pin)) => {
333                        if (w_run - w_pin).abs() > 1e-6 {
334                            return Err(PlanTableError {
335                                shape: shape_str.to_string(),
336                                lane: lane_str.to_string(),
337                                field: "weight".to_string(),
338                                message: format!(
339                                    "weight mismatch: running={w_run}, pinned={w_pin}"
340                                ),
341                            });
342                        }
343                    }
344                    (None, None) => {}
345                    (r, p) => {
346                        return Err(PlanTableError {
347                            shape: shape_str.to_string(),
348                            lane: lane_str.to_string(),
349                            field: "weight".to_string(),
350                            message: format!("weight mismatch: running={r:?}, pinned={p:?}"),
351                        });
352                    }
353                }
354
355                // Check rrf_constant equality
356                match (running_entry.rrf_constant, pinned_entry.rrf_constant) {
357                    (Some(k_run), Some(k_pin)) => {
358                        if (k_run - k_pin).abs() > 1e-6 {
359                            return Err(PlanTableError {
360                                shape: shape_str.to_string(),
361                                lane: lane_str.to_string(),
362                                field: "rrf_constant".to_string(),
363                                message: format!(
364                                    "rrf_constant mismatch: running={k_run}, pinned={k_pin}"
365                                ),
366                            });
367                        }
368                    }
369                    (None, None) => {}
370                    (r, p) => {
371                        return Err(PlanTableError {
372                            shape: shape_str.to_string(),
373                            lane: lane_str.to_string(),
374                            field: "rrf_constant".to_string(),
375                            message: format!("rrf_constant mismatch: running={r:?}, pinned={p:?}"),
376                        });
377                    }
378                }
379
380                // Check plan_order_index equality
381                if running_entry.plan_order_index != pinned_entry.plan_order_index {
382                    return Err(PlanTableError {
383                        shape: shape_str.to_string(),
384                        lane: lane_str.to_string(),
385                        field: "plan_order_index".to_string(),
386                        message: format!(
387                            "plan_order_index mismatch: running={}, pinned={}",
388                            running_entry.plan_order_index, pinned_entry.plan_order_index
389                        ),
390                    });
391                }
392            }
393
394            // Check for extra lanes in pinned table
395            for pinned_lane in pinned_lanes.keys() {
396                if !running_lanes.contains_key(pinned_lane) {
397                    return Err(PlanTableError {
398                        shape: shape_str.to_string(),
399                        lane: pinned_lane.clone(),
400                        field: "pair".to_string(),
401                        message: "extra lane in pinned table".to_string(),
402                    });
403                }
404            }
405        }
406
407        // Check for extra shapes in pinned table
408        for pinned_shape in pinned.entries.keys() {
409            if !self.entries.contains_key(pinned_shape) {
410                return Err(PlanTableError {
411                    shape: pinned_shape.clone(),
412                    lane: "all".to_string(),
413                    field: "pair".to_string(),
414                    message: "extra shape in pinned table".to_string(),
415                });
416            }
417        }
418
419        Ok(())
420    }
421}
422
423/// Embedded pinned plan table constant compiled into the binary.
424///
425/// The pinned table is a product input, so it lives beside this module: a
426/// product build must not reach outside `crates/` (the release image copies
427/// only the crate tree). The benchmark keeps its own copy under
428/// `benchmarks/aft-search/engine-fixtures/plan-table.json`, and
429/// `engine_plan_table_test` asserts the two are byte-identical so neither can
430/// drift from the other.
431pub const PINNED_PLAN_TABLE_JSON: &str =
432    include_str!("../../../assets/search-plan-table.pinned.json");
433
434/// Load and verify the pinned plan table at startup against the running table.
435pub fn verify_pinned_plan_table_at_startup() -> Result<(), PlanTableError> {
436    let running = PlanTable::running_table();
437    let pinned = PlanTable::from_json(PINNED_PLAN_TABLE_JSON)?;
438    running.verify_against(&pinned)
439}
440
441#[cfg(test)]
442mod tests {
443    use super::*;
444
445    #[test]
446    fn running_table_matches_pinned_json() {
447        verify_pinned_plan_table_at_startup()
448            .expect("pinned plan-table.json must match running table");
449    }
450
451    #[test]
452    #[ignore = "fixture regeneration is an explicit maintainer action"]
453    fn regenerate_pinned_plan_table() {
454        let fixture = Path::new(env!("CARGO_MANIFEST_DIR"))
455            .join("../../benchmarks/aft-search/engine-fixtures/plan-table.json");
456        let json = serde_json::to_string_pretty(&PlanTable::running_table())
457            .expect("serialize running plan table");
458        std::fs::write(fixture, format!("{json}\n")).expect("write pinned plan table");
459    }
460
461    #[test]
462    fn detects_missing_pair() {
463        let running = PlanTable::running_table();
464        let mut pinned = PlanTable::running_table();
465        pinned
466            .entries
467            .get_mut("identifier")
468            .unwrap()
469            .remove("lexical");
470
471        let err = running.verify_against(&pinned).unwrap_err();
472        assert_eq!(err.shape, "identifier");
473        assert_eq!(err.lane, "lexical");
474        assert_eq!(err.field, "pair");
475    }
476
477    #[test]
478    fn detects_extra_pair() {
479        let running = PlanTable::running_table();
480        let mut pinned = PlanTable::running_table();
481        pinned.entries.get_mut("identifier").unwrap().insert(
482            "custom".to_string(),
483            LanePlanEntry {
484                weight: Some(0.5),
485                rrf_constant: Some(60.0),
486                plan_order_index: 3,
487            },
488        );
489
490        let err = running.verify_against(&pinned).unwrap_err();
491        assert_eq!(err.shape, "identifier");
492        assert_eq!(err.lane, "custom");
493        assert_eq!(err.field, "pair");
494    }
495
496    #[test]
497    fn detects_weight_change() {
498        let running = PlanTable::running_table();
499        let mut pinned = PlanTable::running_table();
500        pinned
501            .entries
502            .get_mut("identifier")
503            .unwrap()
504            .get_mut("lexical")
505            .unwrap()
506            .weight = Some(0.75);
507
508        let err = running.verify_against(&pinned).unwrap_err();
509        assert_eq!(err.shape, "identifier");
510        assert_eq!(err.lane, "lexical");
511        assert_eq!(err.field, "weight");
512    }
513
514    #[test]
515    fn detects_all_shape_rrf_change() {
516        let running = PlanTable::running_table();
517        let mut pinned = PlanTable::running_table();
518        for (_, lanes) in pinned.entries.iter_mut() {
519            for (lane_name, entry) in lanes.iter_mut() {
520                if lane_name != "exact" {
521                    entry.rrf_constant = Some(50.0);
522                }
523            }
524        }
525
526        let err = running.verify_against(&pinned).unwrap_err();
527        assert_eq!(err.field, "rrf_constant");
528    }
529
530    #[test]
531    fn detects_exact_lane_zero_weight() {
532        let running = PlanTable::running_table();
533        let mut pinned = PlanTable::running_table();
534        pinned
535            .entries
536            .get_mut("identifier")
537            .unwrap()
538            .get_mut("exact")
539            .unwrap()
540            .weight = Some(0.0);
541
542        let err = running.verify_against(&pinned).unwrap_err();
543        assert_eq!(err.shape, "identifier");
544        assert_eq!(err.lane, "exact");
545        assert_eq!(err.field, "weight");
546    }
547}