1use std::collections::HashMap;
37
38#[derive(Clone, Debug, PartialEq, Eq)]
42pub enum ProvenanceKind {
43 RuleFired { rule_id: u64 },
45
46 FactAssertion,
48
49 InferenceStep { step: u32 },
51
52 ExternalInput { source: String },
54
55 Derived { parent_ids: Vec<u64> },
57}
58
59#[derive(Clone, Debug)]
63pub struct ProvenanceRecord {
64 pub record_id: u64,
66
67 pub tensor_id: u64,
69
70 pub kind: ProvenanceKind,
72
73 pub created_at_tick: u64,
75
76 pub confidence: f64,
78}
79
80impl ProvenanceRecord {
81 pub fn is_externally_sourced(&self) -> bool {
84 matches!(
85 self.kind,
86 ProvenanceKind::FactAssertion | ProvenanceKind::ExternalInput { .. }
87 )
88 }
89}
90
91#[derive(Clone, Debug, Default)]
95pub struct ProvenanceChain {
96 pub chain: Vec<ProvenanceRecord>,
98}
99
100impl ProvenanceChain {
101 pub fn root(&self) -> Option<&ProvenanceRecord> {
104 self.chain.first()
105 }
106
107 pub fn tip(&self) -> Option<&ProvenanceRecord> {
110 self.chain.last()
111 }
112
113 pub fn avg_confidence(&self) -> f64 {
116 if self.chain.is_empty() {
117 return 0.0;
118 }
119 let sum: f64 = self.chain.iter().map(|r| r.confidence).sum();
120 sum / self.chain.len() as f64
121 }
122
123 pub fn len(&self) -> usize {
125 self.chain.len()
126 }
127
128 pub fn is_empty(&self) -> bool {
130 self.chain.is_empty()
131 }
132}
133
134#[derive(Clone, Debug)]
138pub struct ProvenanceStats {
139 pub total_records: usize,
141
142 pub unique_tensors: usize,
144
145 pub fact_count: usize,
147
148 pub rule_fired_count: usize,
150
151 pub avg_confidence: f64,
153}
154
155#[derive(Debug, Default)]
162pub struct TensorProvenanceTracker {
163 pub records: HashMap<u64, ProvenanceRecord>,
165
166 pub tensor_records: HashMap<u64, Vec<u64>>,
168
169 pub next_record_id: u64,
171}
172
173impl TensorProvenanceTracker {
174 pub fn new() -> Self {
176 Self {
177 records: HashMap::new(),
178 tensor_records: HashMap::new(),
179 next_record_id: 0,
180 }
181 }
182
183 pub fn record(
187 &mut self,
188 tensor_id: u64,
189 kind: ProvenanceKind,
190 confidence: f64,
191 tick: u64,
192 ) -> u64 {
193 let record_id = self.next_record_id;
194 self.next_record_id += 1;
195
196 let entry = ProvenanceRecord {
197 record_id,
198 tensor_id,
199 kind,
200 created_at_tick: tick,
201 confidence,
202 };
203
204 self.records.insert(record_id, entry);
205 self.tensor_records
206 .entry(tensor_id)
207 .or_default()
208 .push(record_id);
209
210 record_id
211 }
212
213 pub fn chain_for(&self, tensor_id: u64) -> ProvenanceChain {
217 let chain = self
218 .tensor_records
219 .get(&tensor_id)
220 .map(|ids| {
221 ids.iter()
222 .filter_map(|id| self.records.get(id).cloned())
223 .collect()
224 })
225 .unwrap_or_default();
226 ProvenanceChain { chain }
227 }
228
229 pub fn get_record(&self, record_id: u64) -> Option<&ProvenanceRecord> {
231 self.records.get(&record_id)
232 }
233
234 pub fn records_for_tensor(&self, tensor_id: u64) -> Vec<&ProvenanceRecord> {
236 self.tensor_records
237 .get(&tensor_id)
238 .map(|ids| ids.iter().filter_map(|id| self.records.get(id)).collect())
239 .unwrap_or_default()
240 }
241
242 pub fn externally_sourced_tensors(&self) -> Vec<u64> {
246 let mut result: Vec<u64> = self
247 .tensor_records
248 .iter()
249 .filter(|(_tid, ids)| {
250 ids.iter()
251 .filter_map(|id| self.records.get(id))
252 .any(|r| r.is_externally_sourced())
253 })
254 .map(|(tid, _)| *tid)
255 .collect();
256 result.sort_unstable();
257 result
258 }
259
260 pub fn delete_tensor(&mut self, tensor_id: u64) -> bool {
264 match self.tensor_records.remove(&tensor_id) {
265 None => false,
266 Some(ids) => {
267 for id in ids {
268 self.records.remove(&id);
269 }
270 true
271 }
272 }
273 }
274
275 pub fn stats(&self) -> ProvenanceStats {
277 let total_records = self.records.len();
278 let unique_tensors = self.tensor_records.len();
279
280 let mut fact_count = 0usize;
281 let mut rule_fired_count = 0usize;
282 let mut confidence_sum = 0.0_f64;
283
284 for record in self.records.values() {
285 match &record.kind {
286 ProvenanceKind::FactAssertion => fact_count += 1,
287 ProvenanceKind::RuleFired { .. } => rule_fired_count += 1,
288 _ => {}
289 }
290 confidence_sum += record.confidence;
291 }
292
293 let avg_confidence = if total_records == 0 {
294 0.0
295 } else {
296 confidence_sum / total_records as f64
297 };
298
299 ProvenanceStats {
300 total_records,
301 unique_tensors,
302 fact_count,
303 rule_fired_count,
304 avg_confidence,
305 }
306 }
307}
308
309#[cfg(test)]
312mod tests {
313 use super::*;
314
315 #[test]
318 fn new_starts_empty() {
319 let tracker = TensorProvenanceTracker::new();
320 assert!(tracker.records.is_empty());
321 assert!(tracker.tensor_records.is_empty());
322 assert_eq!(tracker.next_record_id, 0);
323 }
324
325 #[test]
328 fn record_stores_and_returns_id() {
329 let mut t = TensorProvenanceTracker::new();
330 let id = t.record(42, ProvenanceKind::FactAssertion, 1.0, 0);
331 assert_eq!(id, 0);
332 assert!(t.records.contains_key(&0));
333 }
334
335 #[test]
336 fn record_increments_id() {
337 let mut t = TensorProvenanceTracker::new();
338 let a = t.record(1, ProvenanceKind::FactAssertion, 1.0, 0);
339 let b = t.record(2, ProvenanceKind::FactAssertion, 1.0, 1);
340 assert_eq!(a, 0);
341 assert_eq!(b, 1);
342 }
343
344 #[test]
345 fn record_appends_to_tensor_records() {
346 let mut t = TensorProvenanceTracker::new();
347 t.record(10, ProvenanceKind::FactAssertion, 1.0, 0);
348 assert_eq!(t.tensor_records[&10], vec![0u64]);
349 }
350
351 #[test]
352 fn multiple_records_for_same_tensor() {
353 let mut t = TensorProvenanceTracker::new();
354 t.record(5, ProvenanceKind::FactAssertion, 0.8, 0);
355 t.record(5, ProvenanceKind::RuleFired { rule_id: 99 }, 0.9, 1);
356 assert_eq!(t.tensor_records[&5], vec![0u64, 1u64]);
357 assert_eq!(t.records.len(), 2);
358 }
359
360 #[test]
363 fn chain_for_returns_in_insertion_order() {
364 let mut t = TensorProvenanceTracker::new();
365 t.record(7, ProvenanceKind::FactAssertion, 0.5, 0);
366 t.record(7, ProvenanceKind::InferenceStep { step: 1 }, 0.7, 1);
367 t.record(7, ProvenanceKind::RuleFired { rule_id: 3 }, 0.9, 2);
368 let chain = t.chain_for(7);
369 assert_eq!(chain.len(), 3);
370 assert_eq!(chain.chain[0].record_id, 0);
371 assert_eq!(chain.chain[1].record_id, 1);
372 assert_eq!(chain.chain[2].record_id, 2);
373 }
374
375 #[test]
376 fn chain_for_empty_chain_for_unknown_tensor() {
377 let t = TensorProvenanceTracker::new();
378 let chain = t.chain_for(999);
379 assert!(chain.is_empty());
380 }
381
382 #[test]
385 fn provenance_chain_root_and_tip() {
386 let mut t = TensorProvenanceTracker::new();
387 t.record(3, ProvenanceKind::FactAssertion, 0.4, 0);
388 t.record(3, ProvenanceKind::RuleFired { rule_id: 1 }, 0.6, 1);
389 let chain = t.chain_for(3);
390 assert_eq!(chain.root().map(|r| r.record_id), Some(0));
391 assert_eq!(chain.tip().map(|r| r.record_id), Some(1));
392 }
393
394 #[test]
395 fn provenance_chain_root_none_when_empty() {
396 let chain = ProvenanceChain::default();
397 assert!(chain.root().is_none());
398 assert!(chain.tip().is_none());
399 }
400
401 #[test]
402 fn provenance_chain_avg_confidence() {
403 let mut t = TensorProvenanceTracker::new();
404 t.record(8, ProvenanceKind::FactAssertion, 0.4, 0);
405 t.record(8, ProvenanceKind::InferenceStep { step: 2 }, 0.6, 1);
406 let chain = t.chain_for(8);
407 let avg = chain.avg_confidence();
408 assert!((avg - 0.5).abs() < 1e-10);
409 }
410
411 #[test]
412 fn provenance_chain_avg_confidence_empty() {
413 let chain = ProvenanceChain::default();
414 assert_eq!(chain.avg_confidence(), 0.0);
415 }
416
417 #[test]
418 fn provenance_chain_len_and_is_empty() {
419 let mut t = TensorProvenanceTracker::new();
420 let empty = t.chain_for(0);
421 assert_eq!(empty.len(), 0);
422 assert!(empty.is_empty());
423
424 t.record(0, ProvenanceKind::FactAssertion, 1.0, 0);
425 let non_empty = t.chain_for(0);
426 assert_eq!(non_empty.len(), 1);
427 assert!(!non_empty.is_empty());
428 }
429
430 #[test]
433 fn get_record_some() {
434 let mut t = TensorProvenanceTracker::new();
435 let id = t.record(1, ProvenanceKind::FactAssertion, 1.0, 5);
436 let rec = t.get_record(id);
437 assert!(rec.is_some());
438 assert_eq!(rec.expect("test: should succeed").tensor_id, 1);
439 assert_eq!(rec.expect("test: should succeed").created_at_tick, 5);
440 }
441
442 #[test]
443 fn get_record_none_for_unknown() {
444 let t = TensorProvenanceTracker::new();
445 assert!(t.get_record(9999).is_none());
446 }
447
448 #[test]
451 fn records_for_tensor_insertion_order() {
452 let mut t = TensorProvenanceTracker::new();
453 t.record(20, ProvenanceKind::FactAssertion, 0.3, 0);
454 t.record(20, ProvenanceKind::RuleFired { rule_id: 7 }, 0.7, 1);
455 let recs = t.records_for_tensor(20);
456 assert_eq!(recs.len(), 2);
457 assert_eq!(recs[0].record_id, 0);
458 assert_eq!(recs[1].record_id, 1);
459 }
460
461 #[test]
462 fn records_for_tensor_empty_for_unknown() {
463 let t = TensorProvenanceTracker::new();
464 assert!(t.records_for_tensor(404).is_empty());
465 }
466
467 #[test]
470 fn is_externally_sourced_fact_assertion_true() {
471 let rec = ProvenanceRecord {
472 record_id: 0,
473 tensor_id: 0,
474 kind: ProvenanceKind::FactAssertion,
475 created_at_tick: 0,
476 confidence: 1.0,
477 };
478 assert!(rec.is_externally_sourced());
479 }
480
481 #[test]
482 fn is_externally_sourced_external_input_true() {
483 let rec = ProvenanceRecord {
484 record_id: 0,
485 tensor_id: 0,
486 kind: ProvenanceKind::ExternalInput {
487 source: "file.csv".to_string(),
488 },
489 created_at_tick: 0,
490 confidence: 0.9,
491 };
492 assert!(rec.is_externally_sourced());
493 }
494
495 #[test]
496 fn is_externally_sourced_rule_fired_false() {
497 let rec = ProvenanceRecord {
498 record_id: 0,
499 tensor_id: 0,
500 kind: ProvenanceKind::RuleFired { rule_id: 1 },
501 created_at_tick: 0,
502 confidence: 0.8,
503 };
504 assert!(!rec.is_externally_sourced());
505 }
506
507 #[test]
508 fn is_externally_sourced_inference_step_false() {
509 let rec = ProvenanceRecord {
510 record_id: 0,
511 tensor_id: 0,
512 kind: ProvenanceKind::InferenceStep { step: 3 },
513 created_at_tick: 0,
514 confidence: 0.7,
515 };
516 assert!(!rec.is_externally_sourced());
517 }
518
519 #[test]
520 fn is_externally_sourced_derived_false() {
521 let rec = ProvenanceRecord {
522 record_id: 0,
523 tensor_id: 0,
524 kind: ProvenanceKind::Derived {
525 parent_ids: vec![1, 2],
526 },
527 created_at_tick: 0,
528 confidence: 0.6,
529 };
530 assert!(!rec.is_externally_sourced());
531 }
532
533 #[test]
536 fn externally_sourced_tensors_correct_and_sorted() {
537 let mut t = TensorProvenanceTracker::new();
538 t.record(5, ProvenanceKind::FactAssertion, 1.0, 0);
540 t.record(
542 2,
543 ProvenanceKind::ExternalInput {
544 source: "db".to_string(),
545 },
546 0.9,
547 1,
548 );
549 t.record(9, ProvenanceKind::RuleFired { rule_id: 0 }, 0.5, 2);
551 t.record(1, ProvenanceKind::InferenceStep { step: 0 }, 0.3, 3);
553 t.record(1, ProvenanceKind::FactAssertion, 0.8, 4);
554
555 let ext = t.externally_sourced_tensors();
556 assert_eq!(ext, vec![1u64, 2u64, 5u64]);
557 }
558
559 #[test]
562 fn delete_tensor_removes_all_records() {
563 let mut t = TensorProvenanceTracker::new();
564 t.record(4, ProvenanceKind::FactAssertion, 1.0, 0);
565 t.record(4, ProvenanceKind::RuleFired { rule_id: 2 }, 0.5, 1);
566 assert_eq!(t.records.len(), 2);
567
568 let ok = t.delete_tensor(4);
569 assert!(ok);
570 assert!(t.records.is_empty());
571 assert!(!t.tensor_records.contains_key(&4));
572 }
573
574 #[test]
575 fn delete_tensor_returns_false_for_unknown() {
576 let mut t = TensorProvenanceTracker::new();
577 assert!(!t.delete_tensor(123));
578 }
579
580 #[test]
583 fn stats_total_records_and_unique_tensors() {
584 let mut t = TensorProvenanceTracker::new();
585 t.record(1, ProvenanceKind::FactAssertion, 1.0, 0);
586 t.record(1, ProvenanceKind::RuleFired { rule_id: 0 }, 0.8, 1);
587 t.record(2, ProvenanceKind::FactAssertion, 0.5, 2);
588 let s = t.stats();
589 assert_eq!(s.total_records, 3);
590 assert_eq!(s.unique_tensors, 2);
591 }
592
593 #[test]
594 fn stats_fact_count_and_rule_fired_count() {
595 let mut t = TensorProvenanceTracker::new();
596 t.record(1, ProvenanceKind::FactAssertion, 1.0, 0);
597 t.record(2, ProvenanceKind::FactAssertion, 1.0, 1);
598 t.record(3, ProvenanceKind::RuleFired { rule_id: 7 }, 0.9, 2);
599 t.record(4, ProvenanceKind::InferenceStep { step: 0 }, 0.6, 3);
600 let s = t.stats();
601 assert_eq!(s.fact_count, 2);
602 assert_eq!(s.rule_fired_count, 1);
603 }
604
605 #[test]
606 fn stats_avg_confidence() {
607 let mut t = TensorProvenanceTracker::new();
608 t.record(1, ProvenanceKind::FactAssertion, 0.8, 0);
609 t.record(2, ProvenanceKind::RuleFired { rule_id: 0 }, 0.4, 1);
610 let s = t.stats();
611 assert!((s.avg_confidence - 0.6).abs() < 1e-10);
612 }
613
614 #[test]
615 fn stats_empty_tracker() {
616 let t = TensorProvenanceTracker::new();
617 let s = t.stats();
618 assert_eq!(s.total_records, 0);
619 assert_eq!(s.unique_tensors, 0);
620 assert_eq!(s.fact_count, 0);
621 assert_eq!(s.rule_fired_count, 0);
622 assert_eq!(s.avg_confidence, 0.0);
623 }
624}