1use std::collections::BTreeMap;
2use std::fmt;
3use std::path::Path;
4
5use serde::{Deserialize, Serialize};
6
7#[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#[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#[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#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
155#[serde(transparent)]
156pub struct PlanTable {
157 pub entries: BTreeMap<String, BTreeMap<String, LanePlanEntry>>,
158}
159
160#[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 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 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 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 pub fn verify_against(&self, pinned: &PlanTable) -> Result<(), PlanTableError> {
251 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 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 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 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 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 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 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
423pub const PINNED_PLAN_TABLE_JSON: &str =
432 include_str!("../../../assets/search-plan-table.pinned.json");
433
434pub 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}