1use crate::error::{Result, Web4Error};
22use serde::{Deserialize, Serialize};
23use std::collections::HashMap;
24use uuid::Uuid;
25
26pub const T3_DIMENSIONS: usize = 3;
28
29#[derive(Clone, Copy, Debug, PartialEq, Eq, Hash, Serialize, Deserialize)]
31#[repr(usize)]
32pub enum TrustDimension {
33 Talent = 0,
35 Training = 1,
37 Temperament = 2,
39}
40
41impl TrustDimension {
42 pub fn all() -> [TrustDimension; T3_DIMENSIONS] {
44 [
45 TrustDimension::Talent,
46 TrustDimension::Training,
47 TrustDimension::Temperament,
48 ]
49 }
50
51 pub fn name(&self) -> &'static str {
53 match self {
54 TrustDimension::Talent => "talent",
55 TrustDimension::Training => "training",
56 TrustDimension::Temperament => "temperament",
57 }
58 }
59}
60
61#[derive(Clone, Debug, Serialize, Deserialize)]
63pub struct SubDimensionScore {
64 pub score: f64,
66 pub weight: f64,
68 pub observation_count: u64,
70 pub parent: TrustDimension,
72}
73
74#[derive(Clone, Debug, Serialize, Deserialize)]
76pub struct T3 {
77 dimensions: [f64; T3_DIMENSIONS],
79
80 weights: [f64; T3_DIMENSIONS],
83
84 observation_counts: [u64; T3_DIMENSIONS],
86
87 #[serde(default, skip_serializing_if = "HashMap::is_empty")]
90 sub_dimensions: HashMap<String, SubDimensionScore>,
91}
92
93impl Default for T3 {
94 fn default() -> Self {
95 Self::new()
96 }
97}
98
99impl T3 {
100 pub fn new() -> Self {
102 Self {
103 dimensions: [0.5; T3_DIMENSIONS],
104 weights: [0.0; T3_DIMENSIONS],
105 observation_counts: [0; T3_DIMENSIONS],
106 sub_dimensions: HashMap::new(),
107 }
108 }
109
110 pub fn with_scores(scores: [f64; T3_DIMENSIONS]) -> Result<Self> {
112 for score in scores {
113 if !(0.0..=1.0).contains(&score) {
114 return Err(Web4Error::InvalidInput(
115 "Trust scores must be in range [0.0, 1.0]".into(),
116 ));
117 }
118 }
119 Ok(Self {
120 dimensions: scores,
121 weights: [0.0; T3_DIMENSIONS],
122 observation_counts: [0; T3_DIMENSIONS],
123 sub_dimensions: HashMap::new(),
124 })
125 }
126
127 pub fn from_parts(
139 scores: [f64; T3_DIMENSIONS],
140 observation_counts: [u64; T3_DIMENSIONS],
141 ) -> Self {
142 let mut dimensions = [0.0; T3_DIMENSIONS];
143 let mut weights = [0.0; T3_DIMENSIONS];
144 for i in 0..T3_DIMENSIONS {
145 dimensions[i] = scores[i].clamp(0.0, 1.0);
146 weights[i] =
147 ((1.0 + observation_counts[i] as f64).ln() / 10.0_f64.ln()).min(1.0);
148 }
149 Self {
150 dimensions,
151 weights,
152 observation_counts,
153 sub_dimensions: HashMap::new(),
154 }
155 }
156
157 pub fn score(&self, dimension: TrustDimension) -> f64 {
159 self.dimensions[dimension as usize]
160 }
161
162 pub fn weight(&self, dimension: TrustDimension) -> f64 {
164 self.weights[dimension as usize]
165 }
166
167 pub fn observation_counts(&self) -> &[u64; T3_DIMENSIONS] {
173 &self.observation_counts
174 }
175
176 pub fn scores(&self) -> &[f64; T3_DIMENSIONS] {
178 &self.dimensions
179 }
180
181 pub fn weights(&self) -> &[f64; T3_DIMENSIONS] {
183 &self.weights
184 }
185
186 pub fn sub_dimensions(&self) -> &HashMap<String, SubDimensionScore> {
188 &self.sub_dimensions
189 }
190
191 pub fn apply_delta(&mut self, dimension: TrustDimension, delta: f64) -> f64 {
200 let idx = dimension as usize;
201 let before = self.dimensions[idx];
202 self.dimensions[idx] = (before + delta).clamp(0.0, 1.0);
203 self.observation_counts[idx] += 1;
204 self.weights[idx] =
205 ((1.0 + self.observation_counts[idx] as f64).ln() / 10.0_f64.ln()).min(1.0);
206 self.dimensions[idx] - before
207 }
208
209 pub fn observe(&mut self, dimension: TrustDimension, observed_score: f64) -> Result<()> {
210 if !(0.0..=1.0).contains(&observed_score) {
211 return Err(Web4Error::InvalidInput(
212 "Observed score must be in range [0.0, 1.0]".into(),
213 ));
214 }
215
216 let idx = dimension as usize;
217 let count = self.observation_counts[idx];
218
219 let alpha = 0.5 / (1.0 + (count as f64 / 10.0));
222 self.dimensions[idx] = alpha * observed_score + (1.0 - alpha) * self.dimensions[idx];
223
224 self.observation_counts[idx] += 1;
226 self.weights[idx] = (1.0 + self.observation_counts[idx] as f64).ln() / 10.0_f64.ln();
227 self.weights[idx] = self.weights[idx].min(1.0);
228
229 Ok(())
230 }
231
232 pub fn observe_sub_dimension(
237 &mut self,
238 name: &str,
239 parent: TrustDimension,
240 observed_score: f64,
241 ) -> Result<()> {
242 if !(0.0..=1.0).contains(&observed_score) {
243 return Err(Web4Error::InvalidInput(
244 "Observed score must be in range [0.0, 1.0]".into(),
245 ));
246 }
247
248 let entry = self.sub_dimensions.entry(name.to_string()).or_insert(
249 SubDimensionScore {
250 score: 0.5,
251 weight: 0.0,
252 observation_count: 0,
253 parent,
254 },
255 );
256
257 let alpha = 0.5 / (1.0 + (entry.observation_count as f64 / 10.0));
258 entry.score = alpha * observed_score + (1.0 - alpha) * entry.score;
259 entry.observation_count += 1;
260 entry.weight = (1.0 + entry.observation_count as f64).ln() / 10.0_f64.ln();
261 entry.weight = entry.weight.min(1.0);
262
263 Ok(())
264 }
265
266 pub fn aggregate(&self) -> f64 {
271 let total_weight: f64 = self.weights.iter().sum();
272 if total_weight == 0.0 {
273 return 0.5; }
275
276 let log_sum: f64 = self
278 .dimensions
279 .iter()
280 .zip(self.weights.iter())
281 .map(|(score, weight)| {
282 weight * (score + 1e-10).ln()
284 })
285 .sum();
286
287 (log_sum / total_weight).exp()
288 }
289
290 pub fn distance(&self, other: &T3) -> f64 {
292 let sum_sq: f64 = self
293 .dimensions
294 .iter()
295 .zip(other.dimensions.iter())
296 .map(|(a, b)| (a - b).powi(2))
297 .sum();
298 sum_sq.sqrt()
299 }
300
301 pub fn merge(&self, other: &T3) -> Self {
305 let mut result = Self::new();
306
307 for i in 0..T3_DIMENSIONS {
308 let total_count = self.observation_counts[i] + other.observation_counts[i];
309 if total_count == 0 {
310 continue;
311 }
312
313 let self_weight = self.observation_counts[i] as f64 / total_count as f64;
314 let other_weight = other.observation_counts[i] as f64 / total_count as f64;
315
316 result.dimensions[i] =
317 self_weight * self.dimensions[i] + other_weight * other.dimensions[i];
318 result.observation_counts[i] = total_count;
319 result.weights[i] = (1.0 + total_count as f64).ln() / 10.0_f64.ln();
320 result.weights[i] = result.weights[i].min(1.0);
321 }
322
323 for (name, sub) in &self.sub_dimensions {
325 result.sub_dimensions.insert(name.clone(), sub.clone());
326 }
327 for (name, other_sub) in &other.sub_dimensions {
328 if let Some(existing) = result.sub_dimensions.get_mut(name) {
329 let total = existing.observation_count + other_sub.observation_count;
330 if total > 0 {
331 let w1 = existing.observation_count as f64 / total as f64;
332 let w2 = other_sub.observation_count as f64 / total as f64;
333 existing.score = w1 * existing.score + w2 * other_sub.score;
334 existing.observation_count = total;
335 existing.weight = (1.0 + total as f64).ln() / 10.0_f64.ln();
336 existing.weight = existing.weight.min(1.0);
337 }
338 } else {
339 result.sub_dimensions.insert(name.clone(), other_sub.clone());
340 }
341 }
342
343 result
344 }
345
346 pub fn decay(&mut self, decay_factor: f64) {
351 for i in 0..T3_DIMENSIONS {
352 let distance_from_neutral = self.dimensions[i] - 0.5;
354 self.dimensions[i] = 0.5 + distance_from_neutral * decay_factor;
355
356 self.weights[i] *= decay_factor;
358 }
359
360 for sub in self.sub_dimensions.values_mut() {
362 let distance = sub.score - 0.5;
363 sub.score = 0.5 + distance * decay_factor;
364 sub.weight *= decay_factor;
365 }
366 }
367
368 pub fn meets_thresholds(&self, min_scores: &[f64; T3_DIMENSIONS]) -> bool {
370 self.dimensions
371 .iter()
372 .zip(min_scores.iter())
373 .all(|(score, min)| score >= min)
374 }
375}
376
377#[derive(Clone, Debug, Serialize, Deserialize)]
379pub struct TrustObservation {
380 pub observer_id: Uuid,
382
383 pub subject_id: Uuid,
385
386 pub dimension: TrustDimension,
388
389 pub score: f64,
391
392 pub context: String,
394
395 pub timestamp: chrono::DateTime<chrono::Utc>,
397}
398
399impl TrustObservation {
400 pub fn new(
402 observer_id: Uuid,
403 subject_id: Uuid,
404 dimension: TrustDimension,
405 score: f64,
406 context: impl Into<String>,
407 ) -> Result<Self> {
408 if !(0.0..=1.0).contains(&score) {
409 return Err(Web4Error::InvalidInput(
410 "Score must be in range [0.0, 1.0]".into(),
411 ));
412 }
413 Ok(Self {
414 observer_id,
415 subject_id,
416 dimension,
417 score,
418 context: context.into(),
419 timestamp: chrono::Utc::now(),
420 })
421 }
422}
423
424#[derive(Clone, Debug, Serialize, Deserialize)]
426pub struct TrustRelation {
427 pub from_id: Uuid,
429
430 pub to_id: Uuid,
432
433 pub tensor: T3,
435
436 pub established_at: chrono::DateTime<chrono::Utc>,
438
439 pub updated_at: chrono::DateTime<chrono::Utc>,
441}
442
443impl TrustRelation {
444 pub fn new(from_id: Uuid, to_id: Uuid) -> Self {
446 let now = chrono::Utc::now();
447 Self {
448 from_id,
449 to_id,
450 tensor: T3::new(),
451 established_at: now,
452 updated_at: now,
453 }
454 }
455
456 pub fn observe(&mut self, dimension: TrustDimension, score: f64) -> Result<()> {
458 self.tensor.observe(dimension, score)?;
459 self.updated_at = chrono::Utc::now();
460 Ok(())
461 }
462
463 pub fn trust_score(&self) -> f64 {
465 self.tensor.aggregate()
466 }
467}
468
469#[cfg(test)]
470mod tests {
471 use super::*;
472
473 #[test]
474 fn test_new_t3_is_neutral() {
475 let t3 = T3::new();
476 assert_eq!(t3.aggregate(), 0.5);
477 for dim in TrustDimension::all() {
478 assert_eq!(t3.score(dim), 0.5);
479 assert_eq!(t3.weight(dim), 0.0);
480 }
481 }
482
483 #[test]
484 fn test_observation_updates_score() {
485 let mut t3 = T3::new();
486 t3.observe(TrustDimension::Talent, 1.0).unwrap();
487
488 assert!(t3.score(TrustDimension::Talent) > 0.5);
490 assert!(t3.weight(TrustDimension::Talent) > 0.0);
492 }
493
494 #[test]
495 fn test_multiple_observations_stabilize() {
496 let mut t3 = T3::new();
497
498 for _ in 0..20 {
500 t3.observe(TrustDimension::Talent, 0.9).unwrap();
501 }
502
503 assert!(t3.score(TrustDimension::Talent) > 0.8);
505 assert!(t3.weight(TrustDimension::Talent) > 0.5);
507 }
508
509 #[test]
510 fn test_invalid_scores_rejected() {
511 let mut t3 = T3::new();
512 assert!(t3.observe(TrustDimension::Talent, 1.5).is_err());
513 assert!(t3.observe(TrustDimension::Talent, -0.1).is_err());
514 }
515
516 #[test]
517 fn test_decay_moves_toward_neutral() {
518 let mut t3 = T3::with_scores([0.9, 0.9, 0.9]).unwrap();
519 t3.decay(0.5);
520
521 for dim in TrustDimension::all() {
522 assert!(t3.score(dim) < 0.9);
524 assert!(t3.score(dim) > 0.5);
525 }
526 }
527
528 #[test]
529 fn test_merge_combines_tensors() {
530 let mut t1 = T3::new();
531 let mut t2 = T3::new();
532
533 for _ in 0..10 {
535 t1.observe(TrustDimension::Talent, 0.9).unwrap();
536 }
537
538 for _ in 0..2 {
540 t2.observe(TrustDimension::Talent, 0.3).unwrap();
541 }
542
543 let merged = t1.merge(&t2);
544
545 assert!(merged.score(TrustDimension::Talent) > 0.7);
547 }
548
549 #[test]
550 fn test_distance_calculation() {
551 let t1 = T3::with_scores([0.0, 0.0, 0.0]).unwrap();
552 let t2 = T3::with_scores([1.0, 1.0, 1.0]).unwrap();
553
554 let dist = t1.distance(&t2);
555 let expected = (3.0_f64).sqrt();
557 assert!((dist - expected).abs() < 0.001);
558 }
559
560 #[test]
561 fn test_threshold_checking() {
562 let t3 = T3::with_scores([0.8, 0.7, 0.6]).unwrap();
563
564 assert!(t3.meets_thresholds(&[0.8, 0.7, 0.6]));
565 assert!(t3.meets_thresholds(&[0.7, 0.6, 0.5]));
566 assert!(!t3.meets_thresholds(&[0.9, 0.7, 0.6]));
567 }
568
569 #[test]
570 fn test_from_parts_roundtrips_scores_and_confidence() {
571 let mut t3 = T3::new();
575 for _ in 0..5 {
576 t3.apply_delta(TrustDimension::Talent, 0.05);
577 }
578 t3.apply_delta(TrustDimension::Training, -0.1);
579
580 let scores = *t3.scores();
581 let counts = *t3.observation_counts();
582 let rebuilt = T3::from_parts(scores, counts);
583
584 for dim in TrustDimension::all() {
585 assert_eq!(rebuilt.score(dim), t3.score(dim));
586 assert_eq!(rebuilt.observation_counts()[dim as usize], counts[dim as usize]);
587 assert!((rebuilt.weight(dim) - t3.weight(dim)).abs() < 1e-12);
588 }
589 }
590
591 #[test]
592 fn test_from_parts_zero_counts_have_zero_weight() {
593 let t3 = T3::from_parts([0.8, 0.75, 0.9], [0, 0, 0]);
595 for dim in TrustDimension::all() {
596 assert_eq!(t3.weight(dim), 0.0);
597 }
598 assert_eq!(t3.score(TrustDimension::Talent), 0.8);
599 }
600
601 #[test]
602 fn test_sub_dimension_observation() {
603 let mut t3 = T3::new();
604
605 t3.observe_sub_dimension("surgical_precision", TrustDimension::Talent, 0.9)
607 .unwrap();
608 t3.observe_sub_dimension("diagnostic_intuition", TrustDimension::Talent, 0.7)
609 .unwrap();
610
611 let subs = t3.sub_dimensions();
612 assert_eq!(subs.len(), 2);
613 assert!(subs["surgical_precision"].score > 0.5);
614 assert_eq!(subs["surgical_precision"].parent, TrustDimension::Talent);
615 }
616}