use chrono::{DateTime, Utc};
use serde::{Deserialize, Serialize};
use std::collections::HashSet;
use uuid::Uuid;
pub type NodeId = Uuid;
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Serialize, Deserialize)]
pub enum StateNodeType {
Language,
Framework,
Domain,
TaskType,
Complexity,
Tag,
Composite,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct StateNode {
pub node_id: NodeId,
pub node_type: StateNodeType,
pub value: String,
pub episode_refs: HashSet<Uuid>,
pub created_at: DateTime<Utc>,
pub last_accessed: DateTime<Utc>,
pub access_count: u64,
pub parent: Option<NodeId>,
pub children: HashSet<NodeId>,
}
impl StateNode {
#[must_use]
pub fn new(node_type: StateNodeType, value: String) -> Self {
Self {
node_id: Uuid::new_v4(),
node_type,
value,
episode_refs: HashSet::new(),
created_at: Utc::now(),
last_accessed: Utc::now(),
access_count: 0,
parent: None,
children: HashSet::new(),
}
}
#[must_use]
pub fn with_id(node_id: NodeId, node_type: StateNodeType, value: String) -> Self {
Self {
node_id,
node_type,
value,
episode_refs: HashSet::new(),
created_at: Utc::now(),
last_accessed: Utc::now(),
access_count: 0,
parent: None,
children: HashSet::new(),
}
}
pub fn add_episode_ref(&mut self, episode_id: Uuid) {
self.episode_refs.insert(episode_id);
}
pub fn remove_episode_ref(&mut self, episode_id: &Uuid) {
self.episode_refs.remove(episode_id);
}
#[must_use]
pub fn ref_count(&self) -> usize {
self.episode_refs.len()
}
#[must_use]
pub fn has_refs(&self) -> bool {
!self.episode_refs.is_empty()
}
pub fn mark_accessed(&mut self) {
self.last_accessed = Utc::now();
self.access_count += 1;
}
#[must_use]
pub fn composite(children: Vec<&StateNode>) -> Self {
let mut node = Self::new(
StateNodeType::Composite,
Self::compute_composite_value(&children),
);
for child in children {
node.children.insert(child.node_id);
}
node
}
fn compute_composite_value(children: &[&StateNode]) -> String {
let parts: Vec<String> = children
.iter()
.map(|n| format!("{}={}", n.node_type_name(), n.value))
.collect();
parts.join(",")
}
#[must_use]
pub fn node_type_name(&self) -> &'static str {
match self.node_type {
StateNodeType::Language => "lang",
StateNodeType::Framework => "framework",
StateNodeType::Domain => "domain",
StateNodeType::TaskType => "task_type",
StateNodeType::Complexity => "complexity",
StateNodeType::Tag => "tag",
StateNodeType::Composite => "composite",
}
}
#[must_use]
pub fn estimated_tokens(&self) -> usize {
(self.value.len() / 4).max(1) + 2
}
#[must_use]
pub fn token_savings(&self) -> usize {
if self.ref_count() <= 1 {
return 0;
}
let per_episode_tokens = self.estimated_tokens();
let old_total = self.ref_count() * per_episode_tokens;
let new_total = per_episode_tokens + self.ref_count(); old_total - new_total
}
}
impl PartialEq for StateNode {
fn eq(&self, other: &Self) -> bool {
self.node_id == other.node_id
}
}
impl Eq for StateNode {}
impl std::hash::Hash for StateNode {
fn hash<H: std::hash::Hasher>(&self, state: &mut H) {
self.node_id.hash(state);
}
}