use std::fmt;
#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord)]
pub enum DegradationLevel {
Minimal,
Embeddings,
Full,
}
impl fmt::Display for DegradationLevel {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
match self {
DegradationLevel::Minimal => write!(f, "Minimal"),
DegradationLevel::Embeddings => write!(f, "Embeddings"),
DegradationLevel::Full => write!(f, "Full"),
}
}
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum KcFeature {
BasicRecall,
SemanticSearch,
TopicDiscovery,
BasicCompilation,
EnhancedCompilation,
ConflictAnalysis,
SmartNaming,
ImportExport,
DecayEvaluation,
}
impl KcFeature {
fn required_level(self) -> DegradationLevel {
match self {
KcFeature::BasicRecall => DegradationLevel::Minimal,
KcFeature::ImportExport => DegradationLevel::Minimal,
KcFeature::DecayEvaluation => DegradationLevel::Minimal,
KcFeature::SemanticSearch => DegradationLevel::Embeddings,
KcFeature::TopicDiscovery => DegradationLevel::Embeddings,
KcFeature::BasicCompilation => DegradationLevel::Embeddings,
KcFeature::EnhancedCompilation => DegradationLevel::Full,
KcFeature::ConflictAnalysis => DegradationLevel::Full,
KcFeature::SmartNaming => DegradationLevel::Full,
}
}
fn display_name(self) -> &'static str {
match self {
KcFeature::BasicRecall => "Basic Recall",
KcFeature::SemanticSearch => "Semantic Search",
KcFeature::TopicDiscovery => "Topic Discovery",
KcFeature::BasicCompilation => "Basic Compilation",
KcFeature::EnhancedCompilation => "Enhanced Compilation",
KcFeature::ConflictAnalysis => "Conflict Analysis",
KcFeature::SmartNaming => "Smart Naming",
KcFeature::ImportExport => "Import/Export",
KcFeature::DecayEvaluation => "Decay Evaluation",
}
}
fn fallback_hint(self) -> Option<&'static str> {
match self {
KcFeature::EnhancedCompilation => {
Some("Falling back to template-based BasicCompilation.")
}
KcFeature::SmartNaming => {
Some("Falling back to entity-frequency heuristic naming.")
}
KcFeature::ConflictAnalysis => {
Some("Falling back to embedding-similarity-only conflict detection (no LLM contradiction check).")
}
KcFeature::SemanticSearch => {
Some("Falling back to text-match search.")
}
_ => None,
}
}
}
impl fmt::Display for KcFeature {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
write!(f, "{}", self.display_name())
}
}
pub struct GracefulDegradation {
level: DegradationLevel,
}
impl GracefulDegradation {
pub fn detect(llm_available: bool, embeddings_available: bool) -> Self {
let level = match (llm_available, embeddings_available) {
(true, true) => DegradationLevel::Full,
(_, true) => DegradationLevel::Embeddings,
_ => DegradationLevel::Minimal,
};
Self { level }
}
pub fn level(&self) -> DegradationLevel {
self.level
}
pub fn is_available(&self, feature: KcFeature) -> bool {
self.level >= feature.required_level()
}
pub fn unavailability_message(&self, feature: KcFeature) -> Option<String> {
if self.is_available(feature) {
return None;
}
let required = feature.required_level();
let mut msg = match self.level {
DegradationLevel::Minimal => {
match required {
DegradationLevel::Embeddings => format!(
"⚠️ {} requires embeddings. Run `engram setup` to download a model (80MB, one-time).",
feature.display_name()
),
DegradationLevel::Full => format!(
"⚠️ {} requires embeddings and an LLM. Run `engram setup` to download an embedding model (80MB, one-time), then set llm.provider in config.",
feature.display_name()
),
DegradationLevel::Minimal => unreachable!(),
}
}
DegradationLevel::Embeddings => {
format!(
"ℹ️ {} works but without LLM enhancement. Set llm.provider in config for full features.",
feature.display_name()
)
}
DegradationLevel::Full => {
unreachable!()
}
};
if let Some(hint) = feature.fallback_hint() {
msg.push(' ');
msg.push_str(hint);
}
Some(msg)
}
pub fn upgrade_instructions(&self) -> Option<String> {
match self.level {
DegradationLevel::Minimal => Some(
"To enable semantic features:\n\
1. Run `engram setup` to download an embedding model (~80MB, one-time).\n\
2. (Optional) Set `llm.provider` in your engram config to enable LLM-enhanced \
compilation, conflict analysis, and smart naming."
.to_string(),
),
DegradationLevel::Embeddings => Some(
"To enable LLM-enhanced features:\n\
• Set `llm.provider` in your engram config (e.g. \"openai\", \"anthropic\", or \"local\").\n\
• Ensure the corresponding API key environment variable is set, or start a local Ollama server."
.to_string(),
),
DegradationLevel::Full => None,
}
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_detect_full() {
let gd = GracefulDegradation::detect(true, true);
assert_eq!(gd.level(), DegradationLevel::Full);
}
#[test]
fn test_detect_embeddings_only() {
let gd = GracefulDegradation::detect(false, true);
assert_eq!(gd.level(), DegradationLevel::Embeddings);
}
#[test]
fn test_detect_minimal() {
let gd = GracefulDegradation::detect(false, false);
assert_eq!(gd.level(), DegradationLevel::Minimal);
let gd2 = GracefulDegradation::detect(true, false);
assert_eq!(gd2.level(), DegradationLevel::Minimal);
}
#[test]
fn test_feature_availability_full() {
let gd = GracefulDegradation::detect(true, true);
let all_features = [
KcFeature::BasicRecall,
KcFeature::SemanticSearch,
KcFeature::TopicDiscovery,
KcFeature::BasicCompilation,
KcFeature::EnhancedCompilation,
KcFeature::ConflictAnalysis,
KcFeature::SmartNaming,
KcFeature::ImportExport,
KcFeature::DecayEvaluation,
];
for feature in &all_features {
assert!(
gd.is_available(*feature),
"{:?} should be available at Full level",
feature
);
}
}
#[test]
fn test_feature_availability_embeddings() {
let gd = GracefulDegradation::detect(false, true);
assert!(gd.is_available(KcFeature::BasicRecall));
assert!(gd.is_available(KcFeature::SemanticSearch));
assert!(gd.is_available(KcFeature::TopicDiscovery));
assert!(gd.is_available(KcFeature::BasicCompilation));
assert!(gd.is_available(KcFeature::ImportExport));
assert!(gd.is_available(KcFeature::DecayEvaluation));
assert!(!gd.is_available(KcFeature::EnhancedCompilation));
assert!(!gd.is_available(KcFeature::ConflictAnalysis));
assert!(!gd.is_available(KcFeature::SmartNaming));
}
#[test]
fn test_feature_availability_minimal() {
let gd = GracefulDegradation::detect(false, false);
assert!(gd.is_available(KcFeature::BasicRecall));
assert!(gd.is_available(KcFeature::ImportExport));
assert!(gd.is_available(KcFeature::DecayEvaluation));
assert!(!gd.is_available(KcFeature::SemanticSearch));
assert!(!gd.is_available(KcFeature::TopicDiscovery));
assert!(!gd.is_available(KcFeature::BasicCompilation));
assert!(!gd.is_available(KcFeature::EnhancedCompilation));
assert!(!gd.is_available(KcFeature::ConflictAnalysis));
assert!(!gd.is_available(KcFeature::SmartNaming));
}
#[test]
fn test_unavailability_messages() {
let gd_minimal = GracefulDegradation::detect(false, false);
let gd_embeddings = GracefulDegradation::detect(false, true);
let gd_full = GracefulDegradation::detect(true, true);
assert!(gd_full
.unavailability_message(KcFeature::EnhancedCompilation)
.is_none());
assert!(gd_minimal
.unavailability_message(KcFeature::BasicRecall)
.is_none());
let msg = gd_minimal
.unavailability_message(KcFeature::SemanticSearch)
.expect("should have a message");
assert!(!msg.is_empty());
assert!(msg.contains("⚠️"));
assert!(msg.contains("Semantic Search"));
let msg = gd_minimal
.unavailability_message(KcFeature::EnhancedCompilation)
.expect("should have a message");
assert!(!msg.is_empty());
assert!(msg.contains("⚠️"));
let msg = gd_embeddings
.unavailability_message(KcFeature::EnhancedCompilation)
.expect("should have a message");
assert!(!msg.is_empty());
assert!(msg.contains("ℹ️"));
assert!(msg.contains("Enhanced Compilation"));
let msg = gd_embeddings
.unavailability_message(KcFeature::SmartNaming)
.expect("should have a message");
assert!(msg.contains("entity-frequency heuristic"));
let msg = gd_minimal
.unavailability_message(KcFeature::SemanticSearch)
.expect("should have a message");
assert!(msg.contains("text-match search"));
let msg = gd_embeddings
.unavailability_message(KcFeature::ConflictAnalysis)
.expect("should have a message");
assert!(msg.contains("embedding-similarity-only"));
}
#[test]
fn test_upgrade_instructions() {
let gd_minimal = GracefulDegradation::detect(false, false);
let gd_embeddings = GracefulDegradation::detect(false, true);
let gd_full = GracefulDegradation::detect(true, true);
let instructions = gd_minimal
.upgrade_instructions()
.expect("should have instructions");
assert!(!instructions.is_empty());
assert!(instructions.contains("engram setup"));
assert!(instructions.contains("llm.provider"));
let instructions = gd_embeddings
.upgrade_instructions()
.expect("should have instructions");
assert!(!instructions.is_empty());
assert!(instructions.contains("llm.provider"));
assert!(gd_full.upgrade_instructions().is_none());
}
}