use super::validate_exact_object_fields;
use crate::core::search_result_document::{OPTIONAL, REQUIRED};
pub(super) fn validate_canonical_search_result(
result: &serde_json::Value,
index: usize,
) -> Result<(), String> {
let context = format!("canonical search result[{index}]");
validate_exact_object_fields(result, &context, REQUIRED, OPTIONAL)?;
if result
.get("calibrationId")
.is_some_and(|value| !value.is_string() && !value.is_null())
{
return Err(format!("{context}.calibrationId must be a string or null"));
}
if !["docId", "why"]
.iter()
.all(|field| result.get(*field).is_some_and(serde_json::Value::is_string))
|| !result
.get("provenance")
.is_some_and(serde_json::Value::is_array)
|| !result
.get("calibrated")
.is_some_and(serde_json::Value::is_boolean)
{
return Err(format!("{context} required field types drifted"));
}
for field in ["score", "relevanceScore"] {
let value = result
.get(field)
.and_then(serde_json::Value::as_f64)
.ok_or_else(|| format!("{context}.{field} must be a number"))?;
if !value.is_finite() {
return Err(format!("{context}.{field} must be finite"));
}
}
let relevance = result["relevanceScore"].as_f64().unwrap_or_default();
if !(0.0..=1.0).contains(&relevance) {
return Err(format!("{context}.relevanceScore must be between 0 and 1"));
}
if !matches!(
result.get("scoreKind").and_then(serde_json::Value::as_str),
Some("query_relative_pool_minmax" | "cosine_similarity" | "rrf_fused" | "reranked")
) || !matches!(
result.get("source").and_then(serde_json::Value::as_str),
Some(
"lexical"
| "semantic_fast"
| "semantic_quality"
| "hash_control"
| "hybrid"
| "reranked"
)
) {
return Err(format!("{context} score/source vocabulary drifted"));
}
let interval = result
.get("scoreInterval")
.and_then(serde_json::Value::as_array)
.ok_or_else(|| format!("{context}.scoreInterval must be an array"))?;
if interval.len() != 2
|| !interval
.iter()
.all(|value| value.as_f64().is_some_and(f64::is_finite))
{
return Err(format!(
"{context}.scoreInterval must contain two finite numbers"
));
}
if !result.get("coverageGuarantee").is_some_and(|value| {
value.is_null()
|| value
.as_f64()
.is_some_and(|number| number.is_finite() && (0.0..=1.0).contains(&number))
}) {
return Err(format!("{context}.coverageGuarantee drifted"));
}
Ok(())
}
#[cfg(test)]
#[path = "search_result_contract_tests.rs"]
mod tests;