use std::fmt;
use serde::{Deserialize, Serialize};
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Serialize, Deserialize, Default)]
#[serde(rename_all = "snake_case")]
pub enum SemanticMode {
#[default]
HybridPreferred,
LexicalOnly,
StrictSemantic,
}
impl fmt::Display for SemanticMode {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
f.write_str(self.as_str())
}
}
impl SemanticMode {
pub fn as_str(self) -> &'static str {
match self {
Self::HybridPreferred => "hybrid_preferred",
Self::LexicalOnly => "lexical_only",
Self::StrictSemantic => "strict_semantic",
}
}
pub fn parse(s: &str) -> Option<Self> {
match s.trim().to_ascii_lowercase().replace('-', "_").as_str() {
"hybrid_preferred" | "hybrid" | "default" | "auto" => Some(Self::HybridPreferred),
"lexical_only" | "lexical" | "lex" | "off" => Some(Self::LexicalOnly),
"strict_semantic" | "strict" | "semantic" => Some(Self::StrictSemantic),
_ => None,
}
}
pub fn should_build_semantic(&self) -> bool {
!matches!(self, Self::LexicalOnly)
}
pub fn requires_semantic(&self) -> bool {
matches!(self, Self::StrictSemantic)
}
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Serialize, Deserialize, Default)]
#[serde(rename_all = "snake_case")]
pub enum ModelDownloadPolicy {
#[default]
OptIn,
BudgetGated,
Automatic,
}
impl fmt::Display for ModelDownloadPolicy {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
f.write_str(self.as_str())
}
}
impl ModelDownloadPolicy {
pub fn as_str(self) -> &'static str {
match self {
Self::OptIn => "opt_in",
Self::BudgetGated => "budget_gated",
Self::Automatic => "automatic",
}
}
pub fn parse(s: &str) -> Option<Self> {
match s.trim().to_ascii_lowercase().replace('-', "_").as_str() {
"opt_in" | "optin" | "manual" => Some(Self::OptIn),
"budget_gated" | "budget" | "gated" => Some(Self::BudgetGated),
"automatic" | "auto" => Some(Self::Automatic),
_ => None,
}
}
}
pub const DEFAULT_FAST_TIER_EMBEDDER: &str = "hash";
pub const DEFAULT_QUALITY_TIER_EMBEDDER: &str = "minilm";
pub const DEFAULT_RERANKER: &str = "ms-marco-minilm";
pub const DEFAULT_FAST_DIMENSION: usize = 256;
pub const DEFAULT_QUALITY_DIMENSION: usize = 384;
pub const DEFAULT_QUALITY_WEIGHT: f32 = 0.7;
pub const DEFAULT_MAX_REFINEMENT_DOCS: usize = 100;
pub const DEFAULT_SEMANTIC_BUDGET_MB: u64 = 500;
pub const MIN_FREE_DISK_MB: u64 = 200;
pub const MAX_MODEL_SIZE_MB: u64 = 300;
pub const DEFAULT_MAX_BACKFILL_THREADS: usize = 1;
pub const DEFAULT_MAX_BACKFILL_RSS_MB: u64 = 256;
pub const DEFAULT_IDLE_DELAY_SECONDS: u64 = 30;
pub const DEFAULT_CHUNK_TIMEOUT_SECONDS: u64 = 120;
pub const SEMANTIC_SCHEMA_VERSION: u32 = 1;
pub const CHUNKING_STRATEGY_VERSION: u32 = 1;
#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
pub struct SemanticPolicy {
pub mode: SemanticMode,
pub download_policy: ModelDownloadPolicy,
pub fast_tier_embedder: String,
pub quality_tier_embedder: String,
pub reranker: String,
pub fast_dimension: usize,
pub quality_dimension: usize,
pub quality_weight: f32,
pub max_refinement_docs: usize,
pub semantic_budget_mb: u64,
pub min_free_disk_mb: u64,
pub max_model_size_mb: u64,
pub max_backfill_threads: usize,
pub max_backfill_rss_mb: u64,
pub idle_delay_seconds: u64,
pub chunk_timeout_seconds: u64,
pub semantic_schema_version: u32,
pub chunking_strategy_version: u32,
}
impl Default for SemanticPolicy {
fn default() -> Self {
Self::compiled_defaults()
}
}
impl SemanticPolicy {
pub fn compiled_defaults() -> Self {
Self {
mode: SemanticMode::default(),
download_policy: ModelDownloadPolicy::default(),
fast_tier_embedder: DEFAULT_FAST_TIER_EMBEDDER.to_owned(),
quality_tier_embedder: DEFAULT_QUALITY_TIER_EMBEDDER.to_owned(),
reranker: DEFAULT_RERANKER.to_owned(),
fast_dimension: DEFAULT_FAST_DIMENSION,
quality_dimension: DEFAULT_QUALITY_DIMENSION,
quality_weight: DEFAULT_QUALITY_WEIGHT,
max_refinement_docs: DEFAULT_MAX_REFINEMENT_DOCS,
semantic_budget_mb: DEFAULT_SEMANTIC_BUDGET_MB,
min_free_disk_mb: MIN_FREE_DISK_MB,
max_model_size_mb: MAX_MODEL_SIZE_MB,
max_backfill_threads: DEFAULT_MAX_BACKFILL_THREADS,
max_backfill_rss_mb: DEFAULT_MAX_BACKFILL_RSS_MB,
idle_delay_seconds: DEFAULT_IDLE_DELAY_SECONDS,
chunk_timeout_seconds: DEFAULT_CHUNK_TIMEOUT_SECONDS,
semantic_schema_version: SEMANTIC_SCHEMA_VERSION,
chunking_strategy_version: CHUNKING_STRATEGY_VERSION,
}
}
fn with_env_lookup(mut self, mut lookup: impl FnMut(&str) -> Option<String>) -> Self {
if let Some(val) = lookup("CASS_SEMANTIC_MODE")
&& let Some(mode) = SemanticMode::parse(&val)
{
self.mode = mode;
}
if let Some(val) = lookup("CASS_SEMANTIC_EMBEDDER") {
match val.trim().to_ascii_lowercase().as_str() {
"hash" => {
self.quality_tier_embedder = "hash".to_owned();
}
other => {
self.quality_tier_embedder = other.to_owned();
}
}
}
if let Some(val) = lookup("CASS_SEMANTIC_DOWNLOAD_POLICY")
&& let Some(policy) = ModelDownloadPolicy::parse(&val)
{
self.download_policy = policy;
}
if let Some(val) = lookup("CASS_SEMANTIC_BUDGET_MB")
&& let Ok(mb) = val.trim().parse::<u64>()
{
self.semantic_budget_mb = mb;
}
if let Some(val) = lookup("CASS_SEMANTIC_MIN_FREE_DISK_MB")
&& let Ok(mb) = val.trim().parse::<u64>()
{
self.min_free_disk_mb = mb;
}
if let Some(val) = lookup("CASS_SEMANTIC_MAX_MODEL_SIZE_MB")
&& let Ok(mb) = val.trim().parse::<u64>()
{
self.max_model_size_mb = mb;
}
if let Some(val) = lookup("CASS_TWO_TIER_FAST_DIM")
&& let Ok(dim) = val.trim().parse()
{
self.fast_dimension = dim;
}
if let Some(val) = lookup("CASS_TWO_TIER_QUALITY_DIM")
&& let Ok(dim) = val.trim().parse()
{
self.quality_dimension = dim;
}
if let Some(val) = lookup("CASS_TWO_TIER_QUALITY_WEIGHT")
&& let Ok(w) = val.trim().parse::<f32>()
{
self.quality_weight = w.clamp(0.0, 1.0);
}
if let Some(val) = lookup("CASS_TWO_TIER_MAX_REFINEMENT")
&& let Ok(max) = val.trim().parse()
{
self.max_refinement_docs = max;
}
if let Some(val) = lookup("CASS_SEMANTIC_MAX_BACKFILL_THREADS")
&& let Ok(n) = val.trim().parse()
{
self.max_backfill_threads = n;
}
if let Some(val) = lookup("CASS_SEMANTIC_MAX_BACKFILL_RSS_MB")
&& let Ok(mb) = val.trim().parse()
{
self.max_backfill_rss_mb = mb;
}
if let Some(val) = lookup("CASS_SEMANTIC_IDLE_DELAY_SECONDS")
&& let Ok(s) = val.trim().parse()
{
self.idle_delay_seconds = s;
}
if let Some(val) = lookup("CASS_SEMANTIC_CHUNK_TIMEOUT_SECONDS")
&& let Ok(s) = val.trim().parse()
{
self.chunk_timeout_seconds = s;
}
self
}
pub fn with_env_overrides(self) -> Self {
self.with_env_lookup(|key| dotenvy::var(key).ok())
}
pub fn with_cli_overrides(mut self, overrides: &CliSemanticOverrides) -> Self {
if let Some(mode) = overrides.mode {
self.mode = mode;
}
if let Some(budget) = overrides.semantic_budget_mb {
self.semantic_budget_mb = budget;
}
if let Some(ref embedder) = overrides.quality_tier_embedder {
self.quality_tier_embedder = embedder.clone();
}
if let Some(threads) = overrides.max_backfill_threads {
self.max_backfill_threads = threads;
}
self
}
pub fn resolve(cli: &CliSemanticOverrides) -> Self {
Self::compiled_defaults()
.with_env_overrides()
.with_cli_overrides(cli)
}
}
#[derive(Debug, Clone, Default)]
pub struct CliSemanticOverrides {
pub mode: Option<SemanticMode>,
pub semantic_budget_mb: Option<u64>,
pub quality_tier_embedder: Option<String>,
pub max_backfill_threads: Option<usize>,
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Serialize, Deserialize)]
#[serde(rename_all = "snake_case")]
pub enum SettingSource {
CompiledDefault,
Config,
Environment,
Cli,
}
impl fmt::Display for SettingSource {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
f.write_str(self.as_str())
}
}
impl SettingSource {
pub fn as_str(self) -> &'static str {
match self {
Self::CompiledDefault => "compiled_default",
Self::Config => "config",
Self::Environment => "environment",
Self::Cli => "cli",
}
}
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct EffectiveSetting {
pub name: String,
pub value: String,
pub source: SettingSource,
pub env_var: Option<String>,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct EffectiveSettings {
pub settings: Vec<EffectiveSetting>,
}
fn compiled_default_setting(name: &str, value: impl Into<String>) -> EffectiveSetting {
EffectiveSetting {
name: name.to_owned(),
value: value.into(),
source: SettingSource::CompiledDefault,
env_var: None,
}
}
impl EffectiveSettings {
fn resolve_with_env_lookup(
cli: &CliSemanticOverrides,
lookup: impl FnMut(&str) -> Option<String>,
) -> Self {
let defaults = SemanticPolicy::compiled_defaults();
let env_policy = defaults.clone().with_env_lookup(lookup);
let final_policy = env_policy.clone().with_cli_overrides(cli);
let mut settings = Vec::new();
macro_rules! track {
($name:expr, $field:ident, $env_var:expr, $cli_field:ident) => {
let source = if cli.$cli_field.is_some() {
SettingSource::Cli
} else if env_policy.$field != defaults.$field {
SettingSource::Environment
} else {
SettingSource::CompiledDefault
};
settings.push(EffectiveSetting {
name: $name.to_owned(),
value: format!("{}", final_policy.$field),
source,
env_var: Some($env_var.to_owned()),
});
};
}
track!("mode", mode, "CASS_SEMANTIC_MODE", mode);
track!(
"semantic_budget_mb",
semantic_budget_mb,
"CASS_SEMANTIC_BUDGET_MB",
semantic_budget_mb
);
track!(
"quality_tier_embedder",
quality_tier_embedder,
"CASS_SEMANTIC_EMBEDDER",
quality_tier_embedder
);
track!(
"max_backfill_threads",
max_backfill_threads,
"CASS_SEMANTIC_MAX_BACKFILL_THREADS",
max_backfill_threads
);
settings.push(compiled_default_setting(
"fast_tier_embedder",
final_policy.fast_tier_embedder.clone(),
));
settings.push(compiled_default_setting(
"reranker",
final_policy.reranker.clone(),
));
type EnvOnlyFieldGetter = fn(&SemanticPolicy) -> String;
type EnvOnlyField<'a> = (&'a str, &'a str, EnvOnlyFieldGetter);
let env_only_fields: &[EnvOnlyField<'_>] = &[
("fast_dimension", "CASS_TWO_TIER_FAST_DIM", |p| {
p.fast_dimension.to_string()
}),
("quality_dimension", "CASS_TWO_TIER_QUALITY_DIM", |p| {
p.quality_dimension.to_string()
}),
("quality_weight", "CASS_TWO_TIER_QUALITY_WEIGHT", |p| {
format!("{}", p.quality_weight)
}),
("max_refinement_docs", "CASS_TWO_TIER_MAX_REFINEMENT", |p| {
p.max_refinement_docs.to_string()
}),
("min_free_disk_mb", "CASS_SEMANTIC_MIN_FREE_DISK_MB", |p| {
p.min_free_disk_mb.to_string()
}),
(
"max_model_size_mb",
"CASS_SEMANTIC_MAX_MODEL_SIZE_MB",
|p| p.max_model_size_mb.to_string(),
),
("download_policy", "CASS_SEMANTIC_DOWNLOAD_POLICY", |p| {
p.download_policy.to_string()
}),
(
"idle_delay_seconds",
"CASS_SEMANTIC_IDLE_DELAY_SECONDS",
|p| p.idle_delay_seconds.to_string(),
),
(
"chunk_timeout_seconds",
"CASS_SEMANTIC_CHUNK_TIMEOUT_SECONDS",
|p| p.chunk_timeout_seconds.to_string(),
),
(
"max_backfill_rss_mb",
"CASS_SEMANTIC_MAX_BACKFILL_RSS_MB",
|p| p.max_backfill_rss_mb.to_string(),
),
];
for (name, env_var, getter) in env_only_fields {
let default_val = getter(&defaults);
let env_val = getter(&env_policy);
let source = if env_val != default_val {
SettingSource::Environment
} else {
SettingSource::CompiledDefault
};
settings.push(EffectiveSetting {
name: name.to_string(),
value: getter(&final_policy),
source,
env_var: Some(env_var.to_string()),
});
}
settings.push(compiled_default_setting(
"semantic_schema_version",
final_policy.semantic_schema_version.to_string(),
));
settings.push(compiled_default_setting(
"chunking_strategy_version",
final_policy.chunking_strategy_version.to_string(),
));
Self { settings }
}
pub fn resolve(cli: &CliSemanticOverrides) -> Self {
Self::resolve_with_env_lookup(cli, |key| dotenvy::var(key).ok())
}
pub fn get(&self, name: &str) -> Option<&EffectiveSetting> {
self.settings.iter().find(|s| s.name == name)
}
pub fn source_counts(&self) -> std::collections::HashMap<SettingSource, usize> {
let mut counts = std::collections::HashMap::new();
for s in &self.settings {
*counts.entry(s.source).or_insert(0) += 1;
}
counts
}
}
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
#[serde(rename_all = "snake_case")]
pub enum SemanticCapability {
FullQuality,
QualityNoHnsw,
FastTierOnly,
LexicalOnly,
Degraded { reason: String },
}
impl SemanticCapability {
pub fn can_search_semantic(&self) -> bool {
matches!(
self,
Self::FullQuality | Self::QualityNoHnsw | Self::FastTierOnly
)
}
pub fn has_quality_tier(&self) -> bool {
matches!(self, Self::FullQuality | Self::QualityNoHnsw)
}
pub fn status_label(&self) -> &'static str {
match self {
Self::FullQuality => "SEM+",
Self::QualityNoHnsw => "SEM",
Self::FastTierOnly => "SEM*",
Self::LexicalOnly => "LEX",
Self::Degraded { .. } => "ERR",
}
}
pub fn summary(&self) -> String {
match self {
Self::FullQuality => {
"Full semantic: ML embedder + vector index + HNSW accelerator".to_owned()
}
Self::QualityNoHnsw => {
"Quality semantic: ML embedder + vector index (brute-force)".to_owned()
}
Self::FastTierOnly => {
"Fast semantic: hash embedder only (install ML model for quality)".to_owned()
}
Self::LexicalOnly => "Lexical only: semantic search disabled by policy".to_owned(),
Self::Degraded { reason } => format!("Degraded: {reason}"),
}
}
}
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
#[serde(rename_all = "snake_case")]
pub enum InvalidationAction {
UpToDate,
RebuildInBackground,
DiscardAndRebuild { reason: String },
Evict,
}
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
pub struct SemanticAssetManifest {
pub embedder_id: String,
pub model_revision: String,
pub schema_version: u32,
pub chunking_version: u32,
pub doc_count: u64,
pub built_at_ms: i64,
}
impl SemanticAssetManifest {
pub fn invalidation_action(
&self,
policy: &SemanticPolicy,
current_model_revision: &str,
expected_embedder_id: &str,
) -> InvalidationAction {
if !policy.mode.should_build_semantic() {
return InvalidationAction::Evict;
}
if self.schema_version != policy.semantic_schema_version {
return InvalidationAction::DiscardAndRebuild {
reason: format!(
"semantic schema version changed ({} → {})",
self.schema_version, policy.semantic_schema_version
),
};
}
if self.chunking_version != policy.chunking_strategy_version {
return InvalidationAction::DiscardAndRebuild {
reason: format!(
"chunking strategy version changed ({} → {})",
self.chunking_version, policy.chunking_strategy_version
),
};
}
if self.embedder_id != expected_embedder_id {
return InvalidationAction::DiscardAndRebuild {
reason: format!(
"embedder changed ({} → {})",
self.embedder_id, expected_embedder_id
),
};
}
if self.model_revision != current_model_revision {
return InvalidationAction::RebuildInBackground;
}
InvalidationAction::UpToDate
}
}
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
#[serde(rename_all = "snake_case")]
pub enum BudgetDecision {
Allowed,
OverBudgetWarn { used_mb: u64, budget_mb: u64 },
DiskPressureDeny { free_mb: u64, min_required_mb: u64 },
ModelTooLarge { model_mb: u64, max_mb: u64 },
}
impl BudgetDecision {
pub fn is_allowed(&self) -> bool {
matches!(self, Self::Allowed | Self::OverBudgetWarn { .. })
}
}
impl SemanticPolicy {
pub fn check_budget(
&self,
write_size_mb: u64,
current_semantic_usage_mb: u64,
free_disk_mb: u64,
) -> BudgetDecision {
if write_size_mb > self.max_model_size_mb {
return BudgetDecision::ModelTooLarge {
model_mb: write_size_mb,
max_mb: self.max_model_size_mb,
};
}
if free_disk_mb.saturating_sub(write_size_mb) < self.min_free_disk_mb {
return BudgetDecision::DiskPressureDeny {
free_mb: free_disk_mb,
min_required_mb: self.min_free_disk_mb,
};
}
let new_total = current_semantic_usage_mb.saturating_add(write_size_mb);
if new_total > self.semantic_budget_mb {
return BudgetDecision::OverBudgetWarn {
used_mb: new_total,
budget_mb: self.semantic_budget_mb,
};
}
BudgetDecision::Allowed
}
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct SemanticCapabilityReport {
pub mode: SemanticMode,
pub capability: SemanticCapability,
pub fast_tier_embedder: String,
pub quality_tier_embedder: String,
pub reranker: String,
pub fast_dimension: usize,
pub quality_dimension: usize,
pub quality_weight: f32,
pub semantic_budget_mb: u64,
pub current_usage_mb: u64,
pub download_policy: ModelDownloadPolicy,
pub semantic_schema_version: u32,
pub chunking_strategy_version: u32,
pub summary: String,
}
impl SemanticCapabilityReport {
pub fn from_policy(
policy: &SemanticPolicy,
capability: SemanticCapability,
current_usage_mb: u64,
) -> Self {
let summary = capability.summary();
Self {
mode: policy.mode,
capability,
fast_tier_embedder: policy.fast_tier_embedder.clone(),
quality_tier_embedder: policy.quality_tier_embedder.clone(),
reranker: policy.reranker.clone(),
fast_dimension: policy.fast_dimension,
quality_dimension: policy.quality_dimension,
quality_weight: policy.quality_weight,
semantic_budget_mb: policy.semantic_budget_mb,
current_usage_mb,
download_policy: policy.download_policy,
semantic_schema_version: policy.semantic_schema_version,
chunking_strategy_version: policy.chunking_strategy_version,
summary,
}
}
}
pub const EVICTION_ORDER: &[SemanticArtifactKind] = &[
SemanticArtifactKind::HnswAccelerator,
SemanticArtifactKind::QualityVectorIndex,
SemanticArtifactKind::FastVectorIndex,
SemanticArtifactKind::ModelFiles,
];
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Serialize, Deserialize)]
#[serde(rename_all = "snake_case")]
pub enum SemanticArtifactKind {
HnswAccelerator,
QualityVectorIndex,
FastVectorIndex,
ModelFiles,
}
impl SemanticArtifactKind {
pub fn required_for(&self, capability: &SemanticCapability) -> bool {
match (self, capability) {
(_, SemanticCapability::LexicalOnly) => false,
(Self::HnswAccelerator, _) => false, (Self::ModelFiles, SemanticCapability::FastTierOnly) => false,
(Self::QualityVectorIndex, SemanticCapability::FastTierOnly) => false,
_ => true,
}
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn compiled_defaults_are_hybrid_preferred() {
let p = SemanticPolicy::compiled_defaults();
assert_eq!(p.mode, SemanticMode::HybridPreferred);
assert_eq!(p.fast_tier_embedder, "hash");
assert_eq!(p.quality_tier_embedder, "minilm");
assert_eq!(p.download_policy, ModelDownloadPolicy::OptIn);
assert_eq!(p.fast_dimension, 256);
assert_eq!(p.quality_dimension, 384);
assert!((p.quality_weight - 0.7).abs() < f32::EPSILON);
assert_eq!(p.max_refinement_docs, 100);
assert_eq!(p.semantic_budget_mb, 500);
assert_eq!(p.min_free_disk_mb, 200);
assert_eq!(p.max_backfill_threads, 1);
assert_eq!(p.semantic_schema_version, SEMANTIC_SCHEMA_VERSION);
assert_eq!(p.chunking_strategy_version, CHUNKING_STRATEGY_VERSION);
}
#[test]
fn cli_overrides_beat_defaults() {
let cli = CliSemanticOverrides {
mode: Some(SemanticMode::LexicalOnly),
semantic_budget_mb: Some(100),
quality_tier_embedder: Some("snowflake".to_owned()),
max_backfill_threads: Some(4),
};
let p = SemanticPolicy::compiled_defaults().with_cli_overrides(&cli);
assert_eq!(p.mode, SemanticMode::LexicalOnly);
assert_eq!(p.semantic_budget_mb, 100);
assert_eq!(p.quality_tier_embedder, "snowflake");
assert_eq!(p.max_backfill_threads, 4);
assert_eq!(p.fast_tier_embedder, "hash");
assert_eq!(p.quality_dimension, 384);
}
#[test]
fn cli_overrides_beat_env_overrides() {
let mut p = SemanticPolicy::compiled_defaults();
p.mode = SemanticMode::LexicalOnly; let cli = CliSemanticOverrides {
mode: Some(SemanticMode::StrictSemantic),
..Default::default()
};
let p = p.with_cli_overrides(&cli);
assert_eq!(p.mode, SemanticMode::StrictSemantic);
}
#[test]
fn semantic_mode_parsing() {
let cases: &[(&str, Option<SemanticMode>)] = &[
("hybrid_preferred", Some(SemanticMode::HybridPreferred)),
("hybrid", Some(SemanticMode::HybridPreferred)),
("default", Some(SemanticMode::HybridPreferred)),
("auto", Some(SemanticMode::HybridPreferred)),
("HYBRID", Some(SemanticMode::HybridPreferred)),
("lexical_only", Some(SemanticMode::LexicalOnly)),
("lexical", Some(SemanticMode::LexicalOnly)),
("lex", Some(SemanticMode::LexicalOnly)),
("off", Some(SemanticMode::LexicalOnly)),
("strict_semantic", Some(SemanticMode::StrictSemantic)),
("strict", Some(SemanticMode::StrictSemantic)),
("semantic", Some(SemanticMode::StrictSemantic)),
(" Hybrid-Preferred ", Some(SemanticMode::HybridPreferred)),
("nonsense", None),
("", None),
];
for (input, expected) in cases {
assert_eq!(
SemanticMode::parse(input),
*expected,
"failed for input: {input:?}"
);
}
}
#[test]
fn download_policy_parsing() {
let cases: &[(&str, Option<ModelDownloadPolicy>)] = &[
("opt_in", Some(ModelDownloadPolicy::OptIn)),
("optin", Some(ModelDownloadPolicy::OptIn)),
("manual", Some(ModelDownloadPolicy::OptIn)),
("budget_gated", Some(ModelDownloadPolicy::BudgetGated)),
("budget", Some(ModelDownloadPolicy::BudgetGated)),
("gated", Some(ModelDownloadPolicy::BudgetGated)),
("automatic", Some(ModelDownloadPolicy::Automatic)),
("auto", Some(ModelDownloadPolicy::Automatic)),
("xyz", None),
];
for (input, expected) in cases {
assert_eq!(
ModelDownloadPolicy::parse(input),
*expected,
"failed for input: {input:?}"
);
}
}
#[test]
fn display_spellings_delegate_to_as_str() {
let semantic_modes = [
(SemanticMode::HybridPreferred, "hybrid_preferred"),
(SemanticMode::LexicalOnly, "lexical_only"),
(SemanticMode::StrictSemantic, "strict_semantic"),
];
for (mode, expected) in semantic_modes {
assert_eq!(mode.as_str(), expected);
assert_eq!(mode.to_string(), expected);
}
let download_policies = [
(ModelDownloadPolicy::OptIn, "opt_in"),
(ModelDownloadPolicy::BudgetGated, "budget_gated"),
(ModelDownloadPolicy::Automatic, "automatic"),
];
for (policy, expected) in download_policies {
assert_eq!(policy.as_str(), expected);
assert_eq!(policy.to_string(), expected);
}
let setting_sources = [
(SettingSource::CompiledDefault, "compiled_default"),
(SettingSource::Config, "config"),
(SettingSource::Environment, "environment"),
(SettingSource::Cli, "cli"),
];
for (source, expected) in setting_sources {
assert_eq!(source.as_str(), expected);
assert_eq!(source.to_string(), expected);
}
}
#[test]
fn mode_behaviour_flags() {
let cases: &[(SemanticMode, bool, bool)] = &[
(SemanticMode::HybridPreferred, true, false),
(SemanticMode::LexicalOnly, false, false),
(SemanticMode::StrictSemantic, true, true),
];
for (mode, build, require) in cases {
assert_eq!(
mode.should_build_semantic(),
*build,
"should_build for {mode:?}"
);
assert_eq!(mode.requires_semantic(), *require, "requires for {mode:?}");
}
}
#[test]
fn capability_classification() {
let cases: &[(SemanticCapability, bool, bool, &str)] = &[
(SemanticCapability::FullQuality, true, true, "SEM+"),
(SemanticCapability::QualityNoHnsw, true, true, "SEM"),
(SemanticCapability::FastTierOnly, true, false, "SEM*"),
(SemanticCapability::LexicalOnly, false, false, "LEX"),
(
SemanticCapability::Degraded {
reason: "test".to_owned(),
},
false,
false,
"ERR",
),
];
for (cap, can_search, has_quality, label) in cases {
assert_eq!(
cap.can_search_semantic(),
*can_search,
"can_search for {cap:?}"
);
assert_eq!(
cap.has_quality_tier(),
*has_quality,
"has_quality for {cap:?}"
);
assert_eq!(cap.status_label(), *label, "label for {cap:?}");
}
}
#[test]
fn budget_decisions() {
let p = SemanticPolicy::compiled_defaults();
let cases: &[(u64, u64, u64, BudgetDecision)] = &[
(90, 100, 1000, BudgetDecision::Allowed),
(
90,
450,
1000,
BudgetDecision::OverBudgetWarn {
used_mb: 540,
budget_mb: 500,
},
),
(
90,
0,
250,
BudgetDecision::DiskPressureDeny {
free_mb: 250,
min_required_mb: 200,
},
),
(
350,
0,
1000,
BudgetDecision::ModelTooLarge {
model_mb: 350,
max_mb: 300,
},
),
(90, 410, 1000, BudgetDecision::Allowed),
(
91,
410,
1000,
BudgetDecision::OverBudgetWarn {
used_mb: 501,
budget_mb: 500,
},
),
(90, 0, 290, BudgetDecision::Allowed),
(
90,
0,
289,
BudgetDecision::DiskPressureDeny {
free_mb: 289,
min_required_mb: 200,
},
),
];
for (write, usage, free, expected) in cases {
let got = p.check_budget(*write, *usage, *free);
assert_eq!(
got, *expected,
"budget check failed for write={write}, usage={usage}, free={free}"
);
}
}
#[test]
fn invalidation_decisions() {
let policy = SemanticPolicy::compiled_defaults();
let expected_id = format!(
"{}-{}",
policy.quality_tier_embedder, policy.quality_dimension
);
let base_manifest = SemanticAssetManifest {
embedder_id: expected_id.clone(),
model_revision: "abc123".to_owned(),
schema_version: SEMANTIC_SCHEMA_VERSION,
chunking_version: CHUNKING_STRATEGY_VERSION,
doc_count: 1000,
built_at_ms: 1700000000000,
};
assert_eq!(
base_manifest.invalidation_action(&policy, "abc123", &expected_id),
InvalidationAction::UpToDate,
);
assert_eq!(
base_manifest.invalidation_action(&policy, "def456", &expected_id),
InvalidationAction::RebuildInBackground,
);
{
let mut m = base_manifest.clone();
m.schema_version = 0;
let action = m.invalidation_action(&policy, "abc123", &expected_id);
assert!(matches!(
action,
InvalidationAction::DiscardAndRebuild { .. }
));
}
{
let mut m = base_manifest.clone();
m.chunking_version = 0;
let action = m.invalidation_action(&policy, "abc123", &expected_id);
assert!(matches!(
action,
InvalidationAction::DiscardAndRebuild { .. }
));
}
{
let mut m = base_manifest.clone();
m.embedder_id = "snowflake-768".to_owned();
let action = m.invalidation_action(&policy, "abc123", &expected_id);
assert!(matches!(
action,
InvalidationAction::DiscardAndRebuild { .. }
));
}
{
let mut lex_policy = policy.clone();
lex_policy.mode = SemanticMode::LexicalOnly;
assert_eq!(
base_manifest.invalidation_action(&lex_policy, "abc123", &expected_id),
InvalidationAction::Evict,
);
}
}
#[test]
fn eviction_order_hnsw_first_model_last() {
assert_eq!(EVICTION_ORDER[0], SemanticArtifactKind::HnswAccelerator);
assert_eq!(EVICTION_ORDER[1], SemanticArtifactKind::QualityVectorIndex);
assert_eq!(EVICTION_ORDER[2], SemanticArtifactKind::FastVectorIndex);
assert_eq!(EVICTION_ORDER[3], SemanticArtifactKind::ModelFiles);
}
#[test]
fn artifact_required_for_capability() {
use SemanticArtifactKind::*;
use SemanticCapability::*;
let cases: &[(SemanticArtifactKind, SemanticCapability, bool)] = &[
(HnswAccelerator, FullQuality, false),
(HnswAccelerator, FastTierOnly, false),
(HnswAccelerator, LexicalOnly, false),
(ModelFiles, LexicalOnly, false),
(QualityVectorIndex, LexicalOnly, false),
(FastVectorIndex, LexicalOnly, false),
(FastVectorIndex, FastTierOnly, true),
(QualityVectorIndex, FastTierOnly, false),
(ModelFiles, FastTierOnly, false),
(ModelFiles, FullQuality, true),
(QualityVectorIndex, FullQuality, true),
(FastVectorIndex, FullQuality, true),
];
for (artifact, cap, expected) in cases {
assert_eq!(
artifact.required_for(cap),
*expected,
"{artifact:?} required_for {cap:?}"
);
}
}
#[test]
fn fixture_no_model_state() {
let policy = SemanticPolicy::compiled_defaults();
let cap = SemanticCapability::FastTierOnly;
let report = SemanticCapabilityReport::from_policy(&policy, cap, 0);
assert_eq!(report.mode, SemanticMode::HybridPreferred);
assert!(report.summary.contains("hash embedder only"));
assert_eq!(report.current_usage_mb, 0);
let json = serde_json::to_string_pretty(&report).unwrap();
let deser: SemanticCapabilityReport = serde_json::from_str(&json).unwrap();
assert_eq!(deser.mode, report.mode);
assert_eq!(deser.fast_tier_embedder, "hash");
}
#[test]
fn fixture_fast_tier_only_state() {
let policy = SemanticPolicy::compiled_defaults();
let cap = SemanticCapability::FastTierOnly;
let report = SemanticCapabilityReport::from_policy(&policy, cap, 0);
assert_eq!(report.capability, SemanticCapability::FastTierOnly);
assert_eq!(report.quality_tier_embedder, "minilm");
assert_eq!(report.download_policy, ModelDownloadPolicy::OptIn);
}
#[test]
fn fixture_full_quality_state() {
let policy = SemanticPolicy::compiled_defaults();
let cap = SemanticCapability::FullQuality;
let report = SemanticCapabilityReport::from_policy(&policy, cap, 95);
assert_eq!(report.capability, SemanticCapability::FullQuality);
assert_eq!(report.current_usage_mb, 95);
assert!(report.summary.contains("Full semantic"));
let json = serde_json::to_string_pretty(&report).unwrap();
let deser: SemanticCapabilityReport = serde_json::from_str(&json).unwrap();
assert_eq!(deser.current_usage_mb, 95);
}
#[test]
fn policy_json_round_trip() {
let policy = SemanticPolicy::compiled_defaults();
let json = serde_json::to_string(&policy).unwrap();
let deser: SemanticPolicy = serde_json::from_str(&json).unwrap();
assert_eq!(deser, policy);
}
#[test]
fn asset_manifest_json_round_trip() {
let manifest = SemanticAssetManifest {
embedder_id: "minilm-384".to_owned(),
model_revision: "abc123".to_owned(),
schema_version: 1,
chunking_version: 1,
doc_count: 5000,
built_at_ms: 1700000000000,
};
let json = serde_json::to_string(&manifest).unwrap();
let deser: SemanticAssetManifest = serde_json::from_str(&json).unwrap();
assert_eq!(deser, manifest);
}
#[test]
fn effective_settings_all_defaults() {
let cli = CliSemanticOverrides::default();
let settings = EffectiveSettings::resolve(&cli);
assert!(settings.settings.len() >= 15);
for s in &settings.settings {
assert_eq!(
s.source,
SettingSource::CompiledDefault,
"setting '{}' should be CompiledDefault, got {:?}",
s.name,
s.source
);
}
let mode = settings.get("mode").unwrap();
assert_eq!(mode.value, "hybrid_preferred");
let budget = settings.get("semantic_budget_mb").unwrap();
assert_eq!(budget.value, "500");
assert!(settings.get("fast_tier_embedder").is_some());
assert!(settings.get("reranker").is_some());
assert_eq!(settings.get("reranker").unwrap().value, "ms-marco-minilm");
}
#[test]
fn effective_settings_cli_overrides_show_cli_source() {
let cli = CliSemanticOverrides {
mode: Some(SemanticMode::LexicalOnly),
semantic_budget_mb: Some(100),
..Default::default()
};
let settings = EffectiveSettings::resolve(&cli);
let mode = settings.get("mode").unwrap();
assert_eq!(mode.value, "lexical_only");
assert_eq!(mode.source, SettingSource::Cli);
let budget = settings.get("semantic_budget_mb").unwrap();
assert_eq!(budget.value, "100");
assert_eq!(budget.source, SettingSource::Cli);
let fast_dim = settings.get("fast_dimension").unwrap();
assert_eq!(fast_dim.source, SettingSource::CompiledDefault);
}
#[test]
fn effective_settings_lookup_by_name() {
let cli = CliSemanticOverrides::default();
let settings = EffectiveSettings::resolve(&cli);
assert!(settings.get("mode").is_some());
assert!(settings.get("semantic_schema_version").is_some());
assert!(settings.get("nonexistent").is_none());
}
#[test]
fn effective_settings_environment_overrides_show_environment_source() {
let settings =
EffectiveSettings::resolve_with_env_lookup(&CliSemanticOverrides::default(), |key| {
match key {
"CASS_SEMANTIC_MODE" => Some("lexical_only".to_string()),
"CASS_SEMANTIC_BUDGET_MB" => Some("321".to_string()),
_ => None,
}
});
let mode = settings.get("mode").unwrap();
assert_eq!(mode.value, "lexical_only");
assert_eq!(mode.source, SettingSource::Environment);
let budget = settings.get("semantic_budget_mb").unwrap();
assert_eq!(budget.value, "321");
assert_eq!(budget.source, SettingSource::Environment);
}
#[test]
fn effective_settings_download_policy_uses_snake_case_value() {
let settings =
EffectiveSettings::resolve_with_env_lookup(&CliSemanticOverrides::default(), |key| {
match key {
"CASS_SEMANTIC_DOWNLOAD_POLICY" => Some("budget_gated".to_string()),
_ => None,
}
});
let policy = settings.get("download_policy").unwrap();
assert_eq!(policy.value, "budget_gated");
assert_eq!(policy.source, SettingSource::Environment);
}
#[test]
fn effective_settings_json_round_trip() {
let cli = CliSemanticOverrides {
mode: Some(SemanticMode::StrictSemantic),
..Default::default()
};
let settings = EffectiveSettings::resolve(&cli);
let json = serde_json::to_string_pretty(&settings).unwrap();
let deser: EffectiveSettings = serde_json::from_str(&json).unwrap();
assert_eq!(deser.settings.len(), settings.settings.len());
assert_eq!(deser.get("mode").unwrap().value, "strict_semantic");
}
#[test]
fn effective_settings_source_counts() {
let cli = CliSemanticOverrides {
mode: Some(SemanticMode::LexicalOnly),
semantic_budget_mb: Some(200),
..Default::default()
};
let settings = EffectiveSettings::resolve(&cli);
let counts = settings.source_counts();
assert_eq!(*counts.get(&SettingSource::Cli).unwrap_or(&0), 2);
assert!(*counts.get(&SettingSource::CompiledDefault).unwrap_or(&0) > 10);
}
#[test]
fn effective_settings_version_fields_always_compiled() {
let cli = CliSemanticOverrides::default();
let settings = EffectiveSettings::resolve(&cli);
let schema = settings.get("semantic_schema_version").unwrap();
assert_eq!(schema.source, SettingSource::CompiledDefault);
assert!(schema.env_var.is_none());
let chunking = settings.get("chunking_strategy_version").unwrap();
assert_eq!(chunking.source, SettingSource::CompiledDefault);
assert!(chunking.env_var.is_none());
}
}