1use crate::models::{ChangeDetail, Spec, SpecChange};
4use chrono::Utc;
5use std::collections::HashMap;
6use std::sync::{Arc, Mutex};
7
8pub struct ChangeTracker {
10 history: Arc<Mutex<HashMap<String, Vec<SpecChange>>>>,
12 change_counter: Arc<Mutex<u64>>,
14}
15
16impl ChangeTracker {
17 pub fn new() -> Self {
19 Self {
20 history: Arc::new(Mutex::new(HashMap::new())),
21 change_counter: Arc::new(Mutex::new(0)),
22 }
23 }
24
25 pub fn record_change(
27 &self,
28 spec_id: &str,
29 old: &Spec,
30 new: &Spec,
31 author: Option<String>,
32 rationale: String,
33 ) -> SpecChange {
34 let mut counter = self.change_counter.lock().unwrap();
36 *counter += 1;
37 let change_id = format!("change-{}", counter);
38 drop(counter);
39
40 let changes = Self::detect_changes(old, new);
42
43 let spec_change = SpecChange {
44 id: change_id,
45 spec_id: spec_id.to_string(),
46 timestamp: Utc::now(),
47 author,
48 rationale,
49 changes,
50 };
51
52 let mut history = self.history.lock().unwrap();
54 history
55 .entry(spec_id.to_string())
56 .or_default()
57 .push(spec_change.clone());
58
59 spec_change
60 }
61
62 pub fn get_history(&self, spec_id: &str) -> Vec<SpecChange> {
64 let history = self.history.lock().unwrap();
65 history.get(spec_id).cloned().unwrap_or_default()
66 }
67
68 pub fn get_all_changes(&self) -> Vec<SpecChange> {
70 let history = self.history.lock().unwrap();
71 history
72 .values()
73 .flat_map(|changes| changes.clone())
74 .collect()
75 }
76
77 pub fn clear_history(&self, spec_id: &str) {
79 let mut history = self.history.lock().unwrap();
80 history.remove(spec_id);
81 }
82
83 fn detect_changes(old: &Spec, new: &Spec) -> Vec<ChangeDetail> {
85 let mut changes = Vec::new();
86
87 if old.name != new.name {
89 changes.push(ChangeDetail {
90 field: "name".to_string(),
91 old_value: Some(old.name.clone()),
92 new_value: Some(new.name.clone()),
93 });
94 }
95
96 if old.version != new.version {
98 changes.push(ChangeDetail {
99 field: "version".to_string(),
100 old_value: Some(old.version.clone()),
101 new_value: Some(new.version.clone()),
102 });
103 }
104
105 if old.metadata.phase != new.metadata.phase {
107 changes.push(ChangeDetail {
108 field: "metadata.phase".to_string(),
109 old_value: Some(format!("{:?}", old.metadata.phase)),
110 new_value: Some(format!("{:?}", new.metadata.phase)),
111 });
112 }
113
114 if old.metadata.status != new.metadata.status {
116 changes.push(ChangeDetail {
117 field: "metadata.status".to_string(),
118 old_value: Some(format!("{:?}", old.metadata.status)),
119 new_value: Some(format!("{:?}", new.metadata.status)),
120 });
121 }
122
123 if old.metadata.author != new.metadata.author {
125 changes.push(ChangeDetail {
126 field: "metadata.author".to_string(),
127 old_value: old.metadata.author.clone(),
128 new_value: new.metadata.author.clone(),
129 });
130 }
131
132 if old.requirements.len() != new.requirements.len() {
134 changes.push(ChangeDetail {
135 field: "requirements.count".to_string(),
136 old_value: Some(old.requirements.len().to_string()),
137 new_value: Some(new.requirements.len().to_string()),
138 });
139 }
140
141 let old_has_design = old.design.is_some();
143 let new_has_design = new.design.is_some();
144 if old_has_design != new_has_design {
145 changes.push(ChangeDetail {
146 field: "design".to_string(),
147 old_value: Some(old_has_design.to_string()),
148 new_value: Some(new_has_design.to_string()),
149 });
150 }
151
152 if old.tasks.len() != new.tasks.len() {
154 changes.push(ChangeDetail {
155 field: "tasks.count".to_string(),
156 old_value: Some(old.tasks.len().to_string()),
157 new_value: Some(new.tasks.len().to_string()),
158 });
159 }
160
161 changes
162 }
163}
164
165impl Default for ChangeTracker {
166 fn default() -> Self {
167 Self::new()
168 }
169}
170
171impl Clone for ChangeTracker {
172 fn clone(&self) -> Self {
173 Self {
174 history: Arc::clone(&self.history),
175 change_counter: Arc::clone(&self.change_counter),
176 }
177 }
178}
179
180#[cfg(test)]
181mod tests {
182 use super::*;
183 use crate::models::{SpecMetadata, SpecPhase, SpecStatus};
184
185 #[test]
186 fn test_change_tracker_creation() {
187 let tracker = ChangeTracker::new();
188 let history = tracker.get_history("test-spec");
189 assert_eq!(history.len(), 0);
190 }
191
192 #[test]
193 fn test_record_change_with_name_change() {
194 let tracker = ChangeTracker::new();
195 let now = Utc::now();
196
197 let old_spec = Spec {
198 id: "spec-1".to_string(),
199 name: "Old Name".to_string(),
200 version: "1.0.0".to_string(),
201 requirements: vec![],
202 design: None,
203 tasks: vec![],
204 metadata: SpecMetadata {
205 author: Some("Author".to_string()),
206 created_at: now,
207 updated_at: now,
208 phase: SpecPhase::Requirements,
209 status: SpecStatus::Draft,
210 },
211 inheritance: None,
212 };
213
214 let mut new_spec = old_spec.clone();
215 new_spec.name = "New Name".to_string();
216
217 let change = tracker.record_change(
218 "spec-1",
219 &old_spec,
220 &new_spec,
221 Some("John Doe".to_string()),
222 "Updated spec name".to_string(),
223 );
224
225 assert_eq!(change.spec_id, "spec-1");
226 assert_eq!(change.author, Some("John Doe".to_string()));
227 assert_eq!(change.rationale, "Updated spec name");
228 assert!(!change.changes.is_empty());
229
230 let name_change = change.changes.iter().find(|c| c.field == "name").unwrap();
231 assert_eq!(name_change.old_value, Some("Old Name".to_string()));
232 assert_eq!(name_change.new_value, Some("New Name".to_string()));
233 }
234
235 #[test]
236 fn test_record_change_with_version_change() {
237 let tracker = ChangeTracker::new();
238 let now = Utc::now();
239
240 let old_spec = Spec {
241 id: "spec-1".to_string(),
242 name: "Test".to_string(),
243 version: "1.0.0".to_string(),
244 requirements: vec![],
245 design: None,
246 tasks: vec![],
247 metadata: SpecMetadata {
248 author: None,
249 created_at: now,
250 updated_at: now,
251 phase: SpecPhase::Requirements,
252 status: SpecStatus::Draft,
253 },
254 inheritance: None,
255 };
256
257 let mut new_spec = old_spec.clone();
258 new_spec.version = "1.1.0".to_string();
259
260 let change = tracker.record_change(
261 "spec-1",
262 &old_spec,
263 &new_spec,
264 None,
265 "Version bump".to_string(),
266 );
267
268 let version_change = change
269 .changes
270 .iter()
271 .find(|c| c.field == "version")
272 .unwrap();
273 assert_eq!(version_change.old_value, Some("1.0.0".to_string()));
274 assert_eq!(version_change.new_value, Some("1.1.0".to_string()));
275 }
276
277 #[test]
278 fn test_get_history_preserves_order() {
279 let tracker = ChangeTracker::new();
280 let now = Utc::now();
281
282 let spec = Spec {
283 id: "spec-1".to_string(),
284 name: "Test".to_string(),
285 version: "1.0.0".to_string(),
286 requirements: vec![],
287 design: None,
288 tasks: vec![],
289 metadata: SpecMetadata {
290 author: None,
291 created_at: now,
292 updated_at: now,
293 phase: SpecPhase::Requirements,
294 status: SpecStatus::Draft,
295 },
296 inheritance: None,
297 };
298
299 let mut current = spec.clone();
301 for i in 0..3 {
302 let mut next = current.clone();
303 next.version = format!("1.{}.0", i + 1);
304 tracker.record_change("spec-1", ¤t, &next, None, format!("Change {}", i + 1));
305 current = next;
306 }
307
308 let history = tracker.get_history("spec-1");
309 assert_eq!(history.len(), 3);
310
311 for (i, change) in history.iter().enumerate() {
313 assert_eq!(change.rationale, format!("Change {}", i + 1));
314 }
315 }
316
317 #[test]
318 fn test_change_tracker_with_multiple_specs() {
319 let tracker = ChangeTracker::new();
320 let now = Utc::now();
321
322 let spec1 = Spec {
323 id: "spec-1".to_string(),
324 name: "Spec 1".to_string(),
325 version: "1.0.0".to_string(),
326 requirements: vec![],
327 design: None,
328 tasks: vec![],
329 metadata: SpecMetadata {
330 author: None,
331 created_at: now,
332 updated_at: now,
333 phase: SpecPhase::Requirements,
334 status: SpecStatus::Draft,
335 },
336 inheritance: None,
337 };
338
339 let spec2 = Spec {
340 id: "spec-2".to_string(),
341 name: "Spec 2".to_string(),
342 version: "1.0.0".to_string(),
343 requirements: vec![],
344 design: None,
345 tasks: vec![],
346 metadata: SpecMetadata {
347 author: None,
348 created_at: now,
349 updated_at: now,
350 phase: SpecPhase::Design,
351 status: SpecStatus::Draft,
352 },
353 inheritance: None,
354 };
355
356 let mut spec1_v2 = spec1.clone();
357 spec1_v2.version = "1.1.0".to_string();
358
359 let mut spec2_v2 = spec2.clone();
360 spec2_v2.version = "2.0.0".to_string();
361
362 tracker.record_change("spec-1", &spec1, &spec1_v2, None, "Update 1".to_string());
363 tracker.record_change("spec-2", &spec2, &spec2_v2, None, "Update 2".to_string());
364
365 assert_eq!(tracker.get_history("spec-1").len(), 1);
366 assert_eq!(tracker.get_history("spec-2").len(), 1);
367 assert_eq!(tracker.get_all_changes().len(), 2);
368 }
369
370 #[test]
371 fn test_change_detail_with_no_changes() {
372 let tracker = ChangeTracker::new();
373 let now = Utc::now();
374
375 let spec = Spec {
376 id: "spec-1".to_string(),
377 name: "Test".to_string(),
378 version: "1.0.0".to_string(),
379 requirements: vec![],
380 design: None,
381 tasks: vec![],
382 metadata: SpecMetadata {
383 author: None,
384 created_at: now,
385 updated_at: now,
386 phase: SpecPhase::Requirements,
387 status: SpecStatus::Draft,
388 },
389 inheritance: None,
390 };
391
392 let change = tracker.record_change(
393 "spec-1",
394 &spec,
395 &spec,
396 None,
397 "No actual changes".to_string(),
398 );
399
400 assert_eq!(change.changes.len(), 0);
401 }
402
403 #[test]
404 fn test_change_timestamps_are_recent() {
405 let tracker = ChangeTracker::new();
406 let now = Utc::now();
407
408 let spec = Spec {
409 id: "spec-1".to_string(),
410 name: "Test".to_string(),
411 version: "1.0.0".to_string(),
412 requirements: vec![],
413 design: None,
414 tasks: vec![],
415 metadata: SpecMetadata {
416 author: None,
417 created_at: now,
418 updated_at: now,
419 phase: SpecPhase::Requirements,
420 status: SpecStatus::Draft,
421 },
422 inheritance: None,
423 };
424
425 let mut new_spec = spec.clone();
426 new_spec.version = "1.1.0".to_string();
427
428 let change = tracker.record_change("spec-1", &spec, &new_spec, None, "Update".to_string());
429
430 let time_diff = Utc::now().signed_duration_since(change.timestamp);
432 assert!(time_diff.num_seconds() < 1);
433 }
434
435 #[test]
436 fn test_clear_history() {
437 let tracker = ChangeTracker::new();
438 let now = Utc::now();
439
440 let spec = Spec {
441 id: "spec-1".to_string(),
442 name: "Test".to_string(),
443 version: "1.0.0".to_string(),
444 requirements: vec![],
445 design: None,
446 tasks: vec![],
447 metadata: SpecMetadata {
448 author: None,
449 created_at: now,
450 updated_at: now,
451 phase: SpecPhase::Requirements,
452 status: SpecStatus::Draft,
453 },
454 inheritance: None,
455 };
456
457 let mut new_spec = spec.clone();
458 new_spec.version = "1.1.0".to_string();
459
460 tracker.record_change("spec-1", &spec, &new_spec, None, "Update".to_string());
461 assert_eq!(tracker.get_history("spec-1").len(), 1);
462
463 tracker.clear_history("spec-1");
464 assert_eq!(tracker.get_history("spec-1").len(), 0);
465 }
466
467 #[test]
468 fn test_change_tracker_clone() {
469 let tracker = ChangeTracker::new();
470 let now = Utc::now();
471
472 let spec = Spec {
473 id: "spec-1".to_string(),
474 name: "Test".to_string(),
475 version: "1.0.0".to_string(),
476 requirements: vec![],
477 design: None,
478 tasks: vec![],
479 metadata: SpecMetadata {
480 author: None,
481 created_at: now,
482 updated_at: now,
483 phase: SpecPhase::Requirements,
484 status: SpecStatus::Draft,
485 },
486 inheritance: None,
487 };
488
489 let mut new_spec = spec.clone();
490 new_spec.version = "1.1.0".to_string();
491
492 tracker.record_change("spec-1", &spec, &new_spec, None, "Update".to_string());
493
494 let cloned_tracker = tracker.clone();
495 assert_eq!(cloned_tracker.get_history("spec-1").len(), 1);
496 }
497}