use chrono::Utc;
use serde::{Deserialize, Serialize};
use std::fmt;
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct PersonalFact {
pub id: String,
pub category: PersonalFactCategory,
pub key: String,
pub value: String,
pub context: Option<String>,
pub confidence: f32,
pub reinforcements: u32,
pub contradictions: u32,
pub last_used: i64,
pub created_at: i64,
pub updated_at: i64,
pub source: PersonalFactSource,
#[serde(default)]
pub version: u64,
#[serde(default)]
pub deleted: bool,
#[serde(default)]
pub local_only: bool,
}
impl PersonalFact {
pub fn new(
category: PersonalFactCategory,
key: String,
value: String,
context: Option<String>,
source: PersonalFactSource,
local_only: bool,
) -> Self {
let now = Utc::now().timestamp();
let initial_confidence = source.initial_confidence();
Self {
id: uuid::Uuid::new_v4().to_string(),
category,
key,
value,
context,
confidence: initial_confidence,
reinforcements: 0,
contradictions: 0,
last_used: now,
created_at: now,
updated_at: now,
source,
version: 1,
deleted: false,
local_only,
}
}
pub fn reinforce(&mut self, ema_alpha: f32) {
self.reinforcements += 1;
self.last_used = Utc::now().timestamp();
self.updated_at = self.last_used;
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.updated_at = self.last_used;
self.confidence = ema_alpha * 0.0 + (1.0 - ema_alpha) * self.confidence;
self.version += 1;
}
pub fn update_value(&mut self, new_value: String) {
self.value = new_value;
self.last_used = Utc::now().timestamp();
self.updated_at = self.last_used;
self.version += 1;
}
pub fn apply_decay(&mut self) {
let decay_days = self.category.decay_days();
let now = Utc::now().timestamp();
let days_since_use = (now - self.last_used) as f64 / 86400.0;
if days_since_use > decay_days as f64 {
let excess_days = days_since_use - decay_days as f64;
let decay_factor = 0.99_f64.powf(excess_days);
self.confidence = (self.confidence as f64 * decay_factor) as f32;
}
}
pub fn decayed_confidence(&self) -> f32 {
let decay_days = self.category.decay_days();
let now = Utc::now().timestamp();
let days_since_use = (now - self.last_used) as f64 / 86400.0;
if days_since_use > decay_days as f64 {
let excess_days = days_since_use - decay_days as f64;
let decay_factor = 0.99_f64.powf(excess_days);
(self.confidence as f64 * decay_factor) as f32
} else {
self.confidence
}
}
pub fn is_reliable(&self, min_confidence: f32) -> bool {
self.decayed_confidence() >= min_confidence
}
pub fn delete(&mut self) {
self.deleted = true;
self.updated_at = Utc::now().timestamp();
self.version += 1;
}
pub fn to_context_string(&self) -> String {
if let Some(ref ctx) = self.context {
format!("{}: {} (when {})", self.key, self.value, ctx)
} else {
format!("{}: {}", self.key, self.value)
}
}
}
impl fmt::Display for PersonalFact {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
write!(
f,
"[{:.0}%] {}/{}: {}",
self.confidence * 100.0,
self.category,
self.key,
self.value
)
}
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Serialize, Deserialize)]
#[serde(rename_all = "snake_case")]
pub enum PersonalFactCategory {
Identity,
Preference,
Capability,
Context,
Constraint,
Relationship,
AmbiguityTypePreference,
}
impl PersonalFactCategory {
pub fn all() -> &'static [PersonalFactCategory] {
&[
PersonalFactCategory::Identity,
PersonalFactCategory::Preference,
PersonalFactCategory::Capability,
PersonalFactCategory::Context,
PersonalFactCategory::Constraint,
PersonalFactCategory::Relationship,
PersonalFactCategory::AmbiguityTypePreference,
]
}
pub fn decay_days(&self) -> u32 {
match self {
PersonalFactCategory::Identity => 180,
PersonalFactCategory::Preference => 60,
PersonalFactCategory::Capability => 90,
PersonalFactCategory::Context => 14,
PersonalFactCategory::Constraint => 90,
PersonalFactCategory::Relationship => 60,
PersonalFactCategory::AmbiguityTypePreference => 60,
}
}
pub fn code(&self) -> &'static str {
match self {
PersonalFactCategory::Identity => "id",
PersonalFactCategory::Preference => "pref",
PersonalFactCategory::Capability => "cap",
PersonalFactCategory::Context => "ctx",
PersonalFactCategory::Constraint => "limit",
PersonalFactCategory::Relationship => "rel",
PersonalFactCategory::AmbiguityTypePreference => "amb",
}
}
pub fn description(&self) -> &'static str {
match self {
PersonalFactCategory::Identity => "Identity (name, role, organization)",
PersonalFactCategory::Preference => "Preferences (coding style, tools)",
PersonalFactCategory::Capability => "Capabilities (skills, languages)",
PersonalFactCategory::Context => "Context (current project, recent work)",
PersonalFactCategory::Constraint => "Constraints (limitations, restrictions)",
PersonalFactCategory::Relationship => "Relationships (fact connections)",
PersonalFactCategory::AmbiguityTypePreference => "Ambiguity Type Preferences (AT-CoT)",
}
}
}
impl fmt::Display for PersonalFactCategory {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
write!(f, "{}", self.code())
}
}
impl std::str::FromStr for PersonalFactCategory {
type Err = String;
fn from_str(s: &str) -> Result<Self, Self::Err> {
match s.to_lowercase().as_str() {
"id" | "identity" => Ok(PersonalFactCategory::Identity),
"pref" | "preference" => Ok(PersonalFactCategory::Preference),
"cap" | "capability" => Ok(PersonalFactCategory::Capability),
"ctx" | "context" => Ok(PersonalFactCategory::Context),
"limit" | "constraint" => Ok(PersonalFactCategory::Constraint),
"rel" | "relationship" => Ok(PersonalFactCategory::Relationship),
_ => Err(format!("Unknown category: {}", s)),
}
}
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Serialize, Deserialize)]
#[serde(rename_all = "snake_case")]
pub enum PersonalFactSource {
ExplicitStatement,
InferredFromBehavior,
ProfileSetup,
SystemObserved,
}
impl PersonalFactSource {
pub fn initial_confidence(&self) -> f32 {
match self {
PersonalFactSource::ExplicitStatement => 0.9,
PersonalFactSource::InferredFromBehavior => 0.7,
PersonalFactSource::ProfileSetup => 0.85,
PersonalFactSource::SystemObserved => 0.6,
}
}
pub fn description(&self) -> &'static str {
match self {
PersonalFactSource::ExplicitStatement => "Explicitly stated via /profile",
PersonalFactSource::InferredFromBehavior => "Inferred from conversation",
PersonalFactSource::ProfileSetup => "From profile setup",
PersonalFactSource::SystemObserved => "Observed from usage patterns",
}
}
}
impl fmt::Display for PersonalFactSource {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
let s = match self {
PersonalFactSource::ExplicitStatement => "explicit",
PersonalFactSource::InferredFromBehavior => "inferred",
PersonalFactSource::ProfileSetup => "setup",
PersonalFactSource::SystemObserved => "observed",
};
write!(f, "{}", s)
}
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct PendingFactSubmission {
pub fact: PersonalFact,
pub queued_at: i64,
pub attempts: u32,
pub last_error: Option<String>,
}
impl PendingFactSubmission {
pub fn new(fact: PersonalFact) -> Self {
Self {
fact,
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 PersonalFactFeedback {
pub fact_id: String,
pub is_reinforcement: bool,
pub context: Option<String>,
pub timestamp: i64,
}
impl PersonalFactFeedback {
pub fn reinforcement(fact_id: String, context: Option<String>) -> Self {
Self {
fact_id,
is_reinforcement: true,
context,
timestamp: Utc::now().timestamp(),
}
}
pub fn contradiction(fact_id: String, context: Option<String>) -> Self {
Self {
fact_id,
is_reinforcement: false,
context,
timestamp: Utc::now().timestamp(),
}
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_fact_creation() {
let fact = PersonalFact::new(
PersonalFactCategory::Preference,
"preferred_language".to_string(),
"Rust".to_string(),
None,
PersonalFactSource::ExplicitStatement,
false,
);
assert_eq!(fact.category, PersonalFactCategory::Preference);
assert_eq!(fact.confidence, 0.9); assert_eq!(fact.reinforcements, 0);
assert!(!fact.deleted);
assert!(!fact.local_only);
}
#[test]
fn test_reinforcement() {
let mut fact = PersonalFact::new(
PersonalFactCategory::Context,
"current_project".to_string(),
"brainwires".to_string(),
None,
PersonalFactSource::SystemObserved, false,
);
assert_eq!(fact.confidence, 0.6);
fact.reinforce(0.1);
assert!((fact.confidence - 0.64).abs() < 0.001);
assert_eq!(fact.reinforcements, 1);
}
#[test]
fn test_contradiction() {
let mut fact = PersonalFact::new(
PersonalFactCategory::Preference,
"editor".to_string(),
"VSCode".to_string(),
None,
PersonalFactSource::ExplicitStatement, false,
);
fact.contradict(0.1);
assert!((fact.confidence - 0.81).abs() < 0.001);
assert_eq!(fact.contradictions, 1);
}
#[test]
fn test_category_decay_days() {
assert_eq!(PersonalFactCategory::Identity.decay_days(), 180);
assert_eq!(PersonalFactCategory::Preference.decay_days(), 60);
assert_eq!(PersonalFactCategory::Context.decay_days(), 14);
assert_eq!(PersonalFactCategory::Capability.decay_days(), 90);
}
#[test]
fn test_category_parsing() {
assert_eq!(
"id".parse::<PersonalFactCategory>().unwrap(),
PersonalFactCategory::Identity
);
assert_eq!(
"pref".parse::<PersonalFactCategory>().unwrap(),
PersonalFactCategory::Preference
);
assert_eq!(
"ctx".parse::<PersonalFactCategory>().unwrap(),
PersonalFactCategory::Context
);
assert!("invalid".parse::<PersonalFactCategory>().is_err());
}
#[test]
fn test_source_initial_confidence() {
assert_eq!(
PersonalFactSource::ExplicitStatement.initial_confidence(),
0.9
);
assert_eq!(
PersonalFactSource::InferredFromBehavior.initial_confidence(),
0.7
);
assert_eq!(PersonalFactSource::ProfileSetup.initial_confidence(), 0.85);
assert_eq!(PersonalFactSource::SystemObserved.initial_confidence(), 0.6);
}
#[test]
fn test_fact_display() {
let fact = PersonalFact::new(
PersonalFactCategory::Preference,
"language".to_string(),
"Rust".to_string(),
None,
PersonalFactSource::ExplicitStatement,
false,
);
let display = format!("{}", fact);
assert!(display.contains("90%"));
assert!(display.contains("pref"));
assert!(display.contains("language"));
assert!(display.contains("Rust"));
}
#[test]
fn test_context_string() {
let fact = PersonalFact::new(
PersonalFactCategory::Preference,
"framework".to_string(),
"React".to_string(),
Some("frontend projects".to_string()),
PersonalFactSource::ExplicitStatement,
false,
);
assert_eq!(
fact.to_context_string(),
"framework: React (when frontend projects)"
);
let fact_no_context = PersonalFact::new(
PersonalFactCategory::Identity,
"name".to_string(),
"John".to_string(),
None,
PersonalFactSource::ExplicitStatement,
false,
);
assert_eq!(fact_no_context.to_context_string(), "name: John");
}
}