use chrono::Utc;
use serde::{Deserialize, Serialize};
use std::fmt;
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct BehavioralTruth {
pub id: String,
pub category: TruthCategory,
pub context_pattern: String,
pub rule: String,
pub rationale: String,
pub confidence: f32,
pub reinforcements: u32,
pub contradictions: u32,
pub last_used: i64,
pub created_at: i64,
pub created_by: Option<String>,
pub source: TruthSource,
#[serde(default)]
pub version: u64,
#[serde(default)]
pub deleted: bool,
}
impl BehavioralTruth {
pub fn new(
category: TruthCategory,
context_pattern: String,
rule: String,
rationale: String,
source: TruthSource,
created_by: Option<String>,
) -> Self {
let now = Utc::now().timestamp();
let initial_confidence = source.initial_confidence();
Self {
id: uuid::Uuid::new_v4().to_string(),
category,
context_pattern,
rule,
rationale,
confidence: initial_confidence,
reinforcements: 0,
contradictions: 0,
last_used: now,
created_at: now,
created_by,
source,
version: 1,
deleted: false,
}
}
pub fn reinforce(&mut self, ema_alpha: f32) {
self.reinforcements += 1;
self.last_used = Utc::now().timestamp();
self.confidence = ema_alpha * 1.0 + (1.0 - ema_alpha) * self.confidence;
self.confidence = self.confidence.min(1.0); self.version += 1;
}
pub fn contradict(&mut self, ema_alpha: f32) {
self.contradictions += 1;
self.last_used = Utc::now().timestamp();
self.confidence = ema_alpha * 0.0 + (1.0 - ema_alpha) * self.confidence;
self.version += 1;
}
pub fn apply_decay(&mut self, decay_start_days: u32) {
let now = Utc::now().timestamp();
let days_since_use = (now - self.last_used) as f64 / 86400.0;
if days_since_use > decay_start_days as f64 {
let decay_days = days_since_use - decay_start_days as f64;
let decay_factor = 0.99_f64.powf(decay_days);
self.confidence = (self.confidence as f64 * decay_factor) as f32;
}
}
pub fn decayed_confidence(&self, decay_start_days: u32) -> f32 {
let now = Utc::now().timestamp();
let days_since_use = (now - self.last_used) as f64 / 86400.0;
if days_since_use > decay_start_days as f64 {
let decay_days = days_since_use - decay_start_days as f64;
let decay_factor = 0.99_f64.powf(decay_days);
(self.confidence as f64 * decay_factor) as f32
} else {
self.confidence
}
}
pub fn is_reliable(&self, min_confidence: f32, decay_start_days: u32) -> bool {
self.decayed_confidence(decay_start_days) >= min_confidence
}
pub fn success_rate(&self) -> f32 {
let total = self.reinforcements + self.contradictions;
if total == 0 {
0.5 } else {
self.reinforcements as f32 / total as f32
}
}
pub fn delete(&mut self) {
self.deleted = true;
self.version += 1;
}
}
impl fmt::Display for BehavioralTruth {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
write!(
f,
"[{:.0}%] {}: {}",
self.confidence * 100.0,
self.category,
self.rule
)
}
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Serialize, Deserialize)]
#[serde(rename_all = "snake_case")]
pub enum TruthCategory {
CommandUsage,
TaskStrategy,
ToolBehavior,
ErrorRecovery,
ResourceManagement,
PatternAvoidance,
PromptingTechnique,
ClarifyingQuestions,
}
impl TruthCategory {
pub fn all() -> &'static [TruthCategory] {
&[
TruthCategory::CommandUsage,
TruthCategory::TaskStrategy,
TruthCategory::ToolBehavior,
TruthCategory::ErrorRecovery,
TruthCategory::ResourceManagement,
TruthCategory::PatternAvoidance,
TruthCategory::PromptingTechnique,
TruthCategory::ClarifyingQuestions,
]
}
pub fn code(&self) -> &'static str {
match self {
TruthCategory::CommandUsage => "cmd",
TruthCategory::TaskStrategy => "task",
TruthCategory::ToolBehavior => "tool",
TruthCategory::ErrorRecovery => "error",
TruthCategory::ResourceManagement => "resource",
TruthCategory::PatternAvoidance => "avoid",
TruthCategory::PromptingTechnique => "prompt",
TruthCategory::ClarifyingQuestions => "clarify",
}
}
pub fn description(&self) -> &'static str {
match self {
TruthCategory::CommandUsage => "CLI flags and arguments",
TruthCategory::TaskStrategy => "Task execution strategies",
TruthCategory::ToolBehavior => "Tool-specific knowledge",
TruthCategory::ErrorRecovery => "Error recovery patterns",
TruthCategory::ResourceManagement => "Resource management",
TruthCategory::PatternAvoidance => "Anti-patterns to avoid",
TruthCategory::PromptingTechnique => "Prompting technique effectiveness",
TruthCategory::ClarifyingQuestions => "Clarifying question effectiveness",
}
}
}
impl fmt::Display for TruthCategory {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
write!(f, "{}", self.code())
}
}
impl std::str::FromStr for TruthCategory {
type Err = String;
fn from_str(s: &str) -> Result<Self, Self::Err> {
match s.to_lowercase().as_str() {
"cmd" | "command" | "command_usage" | "commandusage" => Ok(TruthCategory::CommandUsage),
"task" | "strategy" | "task_strategy" | "taskstrategy" => {
Ok(TruthCategory::TaskStrategy)
}
"tool" | "behavior" | "tool_behavior" | "toolbehavior" => {
Ok(TruthCategory::ToolBehavior)
}
"error" | "recovery" | "error_recovery" | "errorrecovery" => {
Ok(TruthCategory::ErrorRecovery)
}
"resource" | "management" | "resource_management" | "resourcemanagement" => {
Ok(TruthCategory::ResourceManagement)
}
"avoid" | "pattern" | "pattern_avoidance" | "patternavoidance" => {
Ok(TruthCategory::PatternAvoidance)
}
_ => Err(format!("Unknown category: {}", s)),
}
}
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Serialize, Deserialize)]
#[serde(rename_all = "snake_case")]
pub enum TruthSource {
ExplicitCommand,
ConversationCorrection,
SuccessPattern,
FailurePattern,
}
impl TruthSource {
pub fn initial_confidence(&self) -> f32 {
match self {
TruthSource::ExplicitCommand => 0.8, TruthSource::ConversationCorrection => 0.6, TruthSource::SuccessPattern => 0.4, TruthSource::FailurePattern => 0.5, }
}
pub fn description(&self) -> &'static str {
match self {
TruthSource::ExplicitCommand => "Explicitly taught via /learn",
TruthSource::ConversationCorrection => "Learned from conversation correction",
TruthSource::SuccessPattern => "Detected from success patterns",
TruthSource::FailurePattern => "Detected from failure patterns",
}
}
}
impl fmt::Display for TruthSource {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
let s = match self {
TruthSource::ExplicitCommand => "explicit",
TruthSource::ConversationCorrection => "correction",
TruthSource::SuccessPattern => "success",
TruthSource::FailurePattern => "failure",
};
write!(f, "{}", s)
}
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct PendingTruthSubmission {
pub truth: BehavioralTruth,
pub queued_at: i64,
pub attempts: u32,
pub last_error: Option<String>,
}
impl PendingTruthSubmission {
pub fn new(truth: BehavioralTruth) -> Self {
Self {
truth,
queued_at: Utc::now().timestamp(),
attempts: 0,
last_error: None,
}
}
pub fn record_attempt(&mut self, error: Option<String>) {
self.attempts += 1;
self.last_error = error;
}
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct TruthFeedback {
pub truth_id: String,
pub is_reinforcement: bool,
pub context: Option<String>,
pub timestamp: i64,
}
impl TruthFeedback {
pub fn reinforcement(truth_id: String, context: Option<String>) -> Self {
Self {
truth_id,
is_reinforcement: true,
context,
timestamp: Utc::now().timestamp(),
}
}
pub fn contradiction(truth_id: String, context: Option<String>) -> Self {
Self {
truth_id,
is_reinforcement: false,
context,
timestamp: Utc::now().timestamp(),
}
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_truth_creation() {
let truth = BehavioralTruth::new(
TruthCategory::CommandUsage,
"pm2 logs".to_string(),
"Use --nostream flag to avoid blocking".to_string(),
"pm2 logs streams indefinitely by default".to_string(),
TruthSource::ExplicitCommand,
Some("user123".to_string()),
);
assert_eq!(truth.category, TruthCategory::CommandUsage);
assert_eq!(truth.confidence, 0.8); assert_eq!(truth.reinforcements, 0);
assert!(!truth.deleted);
}
#[test]
fn test_reinforcement() {
let mut truth = BehavioralTruth::new(
TruthCategory::CommandUsage,
"test".to_string(),
"test rule".to_string(),
"test rationale".to_string(),
TruthSource::FailurePattern, None,
);
assert_eq!(truth.confidence, 0.5);
truth.reinforce(0.1);
assert!((truth.confidence - 0.55).abs() < 0.001);
assert_eq!(truth.reinforcements, 1);
}
#[test]
fn test_contradiction() {
let mut truth = BehavioralTruth::new(
TruthCategory::CommandUsage,
"test".to_string(),
"test rule".to_string(),
"test rationale".to_string(),
TruthSource::ExplicitCommand, None,
);
truth.contradict(0.1);
assert!((truth.confidence - 0.72).abs() < 0.001);
assert_eq!(truth.contradictions, 1);
}
#[test]
fn test_success_rate() {
let mut truth = BehavioralTruth::new(
TruthCategory::TaskStrategy,
"test".to_string(),
"test".to_string(),
"test".to_string(),
TruthSource::SuccessPattern,
None,
);
assert_eq!(truth.success_rate(), 0.5);
truth.reinforcements = 3;
truth.contradictions = 1;
assert_eq!(truth.success_rate(), 0.75); }
#[test]
fn test_category_parsing() {
assert_eq!(
"cmd".parse::<TruthCategory>().unwrap(),
TruthCategory::CommandUsage
);
assert_eq!(
"task".parse::<TruthCategory>().unwrap(),
TruthCategory::TaskStrategy
);
assert_eq!(
"avoid".parse::<TruthCategory>().unwrap(),
TruthCategory::PatternAvoidance
);
assert!("invalid".parse::<TruthCategory>().is_err());
}
#[test]
fn test_source_initial_confidence() {
assert_eq!(TruthSource::ExplicitCommand.initial_confidence(), 0.8);
assert_eq!(
TruthSource::ConversationCorrection.initial_confidence(),
0.6
);
assert_eq!(TruthSource::SuccessPattern.initial_confidence(), 0.4);
assert_eq!(TruthSource::FailurePattern.initial_confidence(), 0.5);
}
#[test]
fn test_truth_display() {
let truth = BehavioralTruth::new(
TruthCategory::CommandUsage,
"pm2 logs".to_string(),
"Use --nostream flag".to_string(),
"Avoids blocking".to_string(),
TruthSource::ExplicitCommand,
None,
);
let display = format!("{}", truth);
assert!(display.contains("80%"));
assert!(display.contains("cmd"));
assert!(display.contains("Use --nostream flag"));
}
}