1use chrono::{DateTime, Utc};
2use serde::{Deserialize, Serialize};
3use sha2::{Digest, Sha256};
4use std::collections::HashMap;
5use uuid::Uuid;
6
7#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq, Hash)]
9pub struct TileId(pub String);
10
11impl TileId {
12 pub fn new() -> Self {
13 Self(Uuid::new_v4().to_string())
14 }
15}
16
17impl std::fmt::Display for TileId {
18 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
19 write!(f, "{}", self.0)
20 }
21}
22
23#[derive(Debug, Clone, Serialize, Deserialize)]
25pub struct Tile {
26 pub id: TileId,
27 pub source_hash: String,
28 pub constraints: HashMap<String, String>,
29 pub summary: String,
30 pub context_required: Vec<String>,
31 pub valence: f64,
32 pub created_at: DateTime<Utc>,
33 pub accessed_at: DateTime<Utc>,
34 pub access_count: u64,
35 pub generation: u32,
36 pub parent_id: Option<TileId>,
37}
38
39impl Tile {
40 pub fn hash_content(content: &str) -> String {
42 let mut hasher = Sha256::new();
43 hasher.update(content.as_bytes());
44 format!("{:x}", hasher.finalize())
45 }
46
47 pub fn touch(&mut self) {
49 self.accessed_at = Utc::now();
50 self.access_count += 1;
51 }
52
53 pub fn age_seconds(&self) -> i64 {
55 Utc::now()
56 .signed_duration_since(self.created_at)
57 .num_seconds()
58 }
59}
60
61#[derive(Debug, Clone, Default)]
63pub struct SalienceMap {
64 pub constraints: HashMap<String, String>,
65 pub context_required: Vec<String>,
66 pub valence: Option<f64>,
67}
68
69#[cfg(test)]
70mod tests {
71 use super::*;
72
73 #[test]
74 fn tile_id_is_unique() {
75 let a = TileId::new();
76 let b = TileId::new();
77 assert_ne!(a, b);
78 }
79
80 #[test]
81 fn hash_content_deterministic() {
82 let h1 = Tile::hash_content("hello world");
83 let h2 = Tile::hash_content("hello world");
84 assert_eq!(h1, h2);
85 assert_eq!(h1.len(), 64); }
87
88 #[test]
89 fn hash_content_different() {
90 let h1 = Tile::hash_content("hello");
91 let h2 = Tile::hash_content("world");
92 assert_ne!(h1, h2);
93 }
94
95 #[test]
96 fn tile_touch_updates_access() {
97 let mut tile = Tile {
98 id: TileId::new(),
99 source_hash: String::new(),
100 constraints: HashMap::new(),
101 summary: "test".into(),
102 context_required: vec![],
103 valence: 0.5,
104 created_at: Utc::now(),
105 accessed_at: Utc::now(),
106 access_count: 0,
107 generation: 0,
108 parent_id: None,
109 };
110 let before = tile.accessed_at;
111 tile.touch();
112 assert_eq!(tile.access_count, 1);
113 assert!(tile.accessed_at >= before);
114 }
115
116 #[test]
117 fn tile_roundtrip_json() {
118 let tile = Tile {
119 id: TileId::new(),
120 source_hash: "abc".into(),
121 constraints: HashMap::from([("key".into(), "val".into())]),
122 summary: "a summary".into(),
123 context_required: vec!["ctx1".into()],
124 valence: 0.8,
125 created_at: Utc::now(),
126 accessed_at: Utc::now(),
127 access_count: 3,
128 generation: 1,
129 parent_id: Some(TileId::new()),
130 };
131 let json = serde_json::to_string(&tile).unwrap();
132 let back: Tile = serde_json::from_str(&json).unwrap();
133 assert_eq!(tile.id, back.id);
134 assert_eq!(tile.summary, back.summary);
135 assert_eq!(tile.generation, back.generation);
136 assert_eq!(tile.parent_id, back.parent_id);
137 }
138}