1use crate::distance::cosine_similarity_fast;
38use crate::trajectory::{TrajectoryCoordinate5D, DLMWeights, AttentionWeights};
39
40#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
42pub enum LinkType {
43 Continuation,
45 Elaboration,
47 Summary,
49 Contradiction,
51 Tangent,
53 Question,
55 Answer,
57 Code,
59 Error,
61 Solution,
63 Meta,
65 Unknown,
67}
68
69impl LinkType {
70 pub fn as_index(&self) -> usize {
72 match self {
73 Self::Continuation => 0,
74 Self::Elaboration => 1,
75 Self::Summary => 2,
76 Self::Contradiction => 3,
77 Self::Tangent => 4,
78 Self::Question => 5,
79 Self::Answer => 6,
80 Self::Code => 7,
81 Self::Error => 8,
82 Self::Solution => 9,
83 Self::Meta => 10,
84 Self::Unknown => 11,
85 }
86 }
87
88 pub fn from_str(s: &str) -> Self {
90 match s.to_lowercase().as_str() {
91 "continuation" | "continue" => Self::Continuation,
92 "elaboration" | "elaborate" | "expand" => Self::Elaboration,
93 "summary" | "summarize" | "compress" => Self::Summary,
94 "contradiction" | "contradict" | "correct" => Self::Contradiction,
95 "tangent" | "aside" | "digression" => Self::Tangent,
96 "question" | "ask" | "query" => Self::Question,
97 "answer" | "reply" | "respond" => Self::Answer,
98 "code" | "implementation" | "impl" => Self::Code,
99 "error" | "problem" | "issue" | "bug" => Self::Error,
100 "solution" | "fix" | "resolve" => Self::Solution,
101 "meta" | "reflection" | "comment" => Self::Meta,
102 _ => Self::Unknown,
103 }
104 }
105
106 pub fn name(&self) -> &'static str {
108 match self {
109 Self::Continuation => "continuation",
110 Self::Elaboration => "elaboration",
111 Self::Summary => "summary",
112 Self::Contradiction => "contradiction",
113 Self::Tangent => "tangent",
114 Self::Question => "question",
115 Self::Answer => "answer",
116 Self::Code => "code",
117 Self::Error => "error",
118 Self::Solution => "solution",
119 Self::Meta => "meta",
120 Self::Unknown => "unknown",
121 }
122 }
123
124 pub fn is_constructive(&self) -> bool {
126 matches!(
127 self,
128 Self::Continuation | Self::Elaboration | Self::Answer | Self::Solution
129 )
130 }
131
132 pub fn is_questioning(&self) -> bool {
134 matches!(self, Self::Question | Self::Error | Self::Contradiction)
135 }
136}
137
138impl Default for LinkType {
139 fn default() -> Self {
140 Self::Unknown
141 }
142}
143
144#[derive(Debug, Clone)]
146pub struct ChainLink {
147 pub coordinate: TrajectoryCoordinate5D,
149
150 pub embedding: Vec<f32>,
152
153 pub link_type: LinkType,
155
156 pub influence: f32,
158
159 pub is_user: bool,
161
162 pub attention: Option<AttentionWeights>,
164
165 pub id: Option<String>,
167}
168
169impl ChainLink {
170 pub fn new(
172 coordinate: TrajectoryCoordinate5D,
173 embedding: Vec<f32>,
174 link_type: LinkType,
175 ) -> Self {
176 Self {
177 coordinate,
178 embedding,
179 link_type,
180 influence: 0.5,
181 is_user: false,
182 attention: None,
183 id: None,
184 }
185 }
186
187 pub fn with_metadata(
189 coordinate: TrajectoryCoordinate5D,
190 embedding: Vec<f32>,
191 link_type: LinkType,
192 influence: f32,
193 is_user: bool,
194 ) -> Self {
195 Self {
196 coordinate,
197 embedding,
198 link_type,
199 influence: influence.clamp(0.0, 1.0),
200 is_user,
201 attention: None,
202 id: None,
203 }
204 }
205
206 pub fn with_attention(mut self, attention: AttentionWeights) -> Self {
208 self.attention = Some(attention);
209 self
210 }
211
212 pub fn with_id(mut self, id: impl Into<String>) -> Self {
214 self.id = Some(id.into());
215 self
216 }
217
218 pub fn semantic_similarity(&self, other: &Self) -> f32 {
220 cosine_similarity_fast(&self.embedding, &other.embedding)
221 }
222
223 pub fn coordinate_distance(&self, other: &Self, weights: &DLMWeights) -> f32 {
225 self.coordinate.dlm_distance(&other.coordinate, weights)
226 }
227}
228
229#[derive(Debug, Clone)]
231pub struct ChainLinkEstimatorConfig {
232 pub baseline_weight: f32,
234
235 pub relationship_weight: f32,
237
238 pub type_weight: f32,
240
241 pub context_weight: f32,
243
244 pub coord_weights: DLMWeights,
246
247 pub type_compatibility: [[f32; 12]; 12],
250}
251
252impl Default for ChainLinkEstimatorConfig {
253 fn default() -> Self {
254 Self {
255 baseline_weight: 0.20,
256 relationship_weight: 0.30,
257 type_weight: 0.20,
258 context_weight: 0.30,
259 coord_weights: DLMWeights::default(),
260 type_compatibility: Self::default_compatibility_matrix(),
261 }
262 }
263}
264
265impl ChainLinkEstimatorConfig {
266 fn default_compatibility_matrix() -> [[f32; 12]; 12] {
270 let mut matrix = [[0.5; 12]; 12];
273
274 for i in 0..12 {
276 matrix[i][i] = 0.8;
277 }
278
279 let high_compat = [
281 (0, 1), (1, 0),
283 (0, 2), (2, 0),
285 (5, 6), (6, 5),
287 (7, 8), (8, 7),
289 (8, 9), (9, 8),
291 (7, 9), (9, 7),
293 ];
294 for (i, j) in high_compat {
295 matrix[i][j] = 0.9;
296 }
297
298 let medium_compat = [
300 (1, 2), (2, 1),
302 (3, 9), (9, 3),
304 (4, 5), (5, 4),
306 ];
307 for (i, j) in medium_compat {
308 matrix[i][j] = 0.7;
309 }
310
311 let low_compat = [
313 (0, 4), (4, 0),
315 (3, 0), (0, 3),
317 ];
318 for (i, j) in low_compat {
319 matrix[i][j] = 0.3;
320 }
321
322 matrix
323 }
324
325 pub fn get_type_compatibility(&self, type_a: LinkType, type_b: LinkType) -> f32 {
327 self.type_compatibility[type_a.as_index()][type_b.as_index()]
328 }
329
330 pub fn with_weights(
332 baseline: f32,
333 relationship: f32,
334 type_weight: f32,
335 context: f32,
336 ) -> Self {
337 let total = baseline + relationship + type_weight + context;
339 Self {
340 baseline_weight: baseline / total,
341 relationship_weight: relationship / total,
342 type_weight: type_weight / total,
343 context_weight: context / total,
344 ..Default::default()
345 }
346 }
347}
348
349#[derive(Debug, Clone)]
351pub struct ChainLinkEstimator {
352 config: ChainLinkEstimatorConfig,
353}
354
355impl ChainLinkEstimator {
356 pub fn new(config: ChainLinkEstimatorConfig) -> Self {
358 Self { config }
359 }
360
361 #[inline]
365 fn compute_baseline(&self, link_a: &ChainLink, link_b: &ChainLink) -> f32 {
366 (1.0 + link_a.semantic_similarity(link_b)) / 2.0
368 }
369
370 #[inline]
374 fn compute_relationship(&self, link_a: &ChainLink, link_b: &ChainLink) -> f32 {
375 let coord_dist = link_a.coordinate_distance(link_b, &self.config.coord_weights);
377 let coord_sim = (-coord_dist).exp(); let influence_avg = (link_a.influence + link_b.influence) / 2.0;
381
382 let attention_score = match (&link_a.attention, &link_b.attention) {
384 (Some(att_a), Some(att_b)) => {
385 if link_a.is_user != link_b.is_user {
387 (att_a.total_mass + att_b.total_mass) / 2.0
388 } else {
389 let entropy_a = att_a.forward_entropy();
391 let entropy_b = att_b.forward_entropy();
392 1.0 / (1.0 + (entropy_a + entropy_b) / 2.0)
394 }
395 }
396 _ => 0.5, };
398
399 0.4 * coord_sim + 0.3 * influence_avg + 0.3 * attention_score
401 }
402
403 #[inline]
407 fn compute_type_based(&self, link_a: &ChainLink, link_b: &ChainLink) -> f32 {
408 self.config
409 .get_type_compatibility(link_a.link_type, link_b.link_type)
410 }
411
412 #[inline]
416 fn compute_context_weighted(&self, link_a: &ChainLink, link_b: &ChainLink) -> f32 {
417 let semantic = (1.0 + link_a.semantic_similarity(link_b)) / 2.0;
418
419 let temporal_diff = (link_a.coordinate.temporal - link_b.coordinate.temporal).abs();
421 let temporal_bonus = 1.0 - temporal_diff; let homo_diff = (link_a.coordinate.homogeneity - link_b.coordinate.homogeneity).abs();
425 let homo_bonus = 1.0 - homo_diff;
426
427 let role_bonus = if link_a.is_user == link_b.is_user {
429 0.1
430 } else {
431 0.0
432 };
433
434 0.5 * semantic + 0.2 * temporal_bonus + 0.2 * homo_bonus + 0.1 * (0.5 + role_bonus)
436 }
437
438 pub fn estimate(&self, link_a: &ChainLink, link_b: &ChainLink) -> f32 {
442 let baseline = self.compute_baseline(link_a, link_b);
443 let relationship = self.compute_relationship(link_a, link_b);
444 let type_based = self.compute_type_based(link_a, link_b);
445 let context = self.compute_context_weighted(link_a, link_b);
446
447 self.config.baseline_weight * baseline
448 + self.config.relationship_weight * relationship
449 + self.config.type_weight * type_based
450 + self.config.context_weight * context
451 }
452
453 pub fn estimate_detailed(&self, link_a: &ChainLink, link_b: &ChainLink) -> ChainLinkEstimate {
455 let baseline = self.compute_baseline(link_a, link_b);
456 let relationship = self.compute_relationship(link_a, link_b);
457 let type_based = self.compute_type_based(link_a, link_b);
458 let context = self.compute_context_weighted(link_a, link_b);
459
460 let total = self.config.baseline_weight * baseline
461 + self.config.relationship_weight * relationship
462 + self.config.type_weight * type_based
463 + self.config.context_weight * context;
464
465 ChainLinkEstimate {
466 total,
467 baseline,
468 relationship,
469 type_based,
470 context,
471 }
472 }
473
474 pub fn config(&self) -> &ChainLinkEstimatorConfig {
476 &self.config
477 }
478}
479
480impl Default for ChainLinkEstimator {
481 fn default() -> Self {
482 Self::new(ChainLinkEstimatorConfig::default())
483 }
484}
485
486#[derive(Debug, Clone, Copy)]
488pub struct ChainLinkEstimate {
489 pub total: f32,
491 pub baseline: f32,
493 pub relationship: f32,
495 pub type_based: f32,
497 pub context: f32,
499}
500
501impl ChainLinkEstimate {
502 pub fn is_strong(&self) -> bool {
504 self.total > 0.7
505 }
506
507 pub fn is_weak(&self) -> bool {
509 self.total < 0.3
510 }
511
512 pub fn dominant_component(&self) -> &'static str {
514 let components = [
515 (self.baseline, "baseline"),
516 (self.relationship, "relationship"),
517 (self.type_based, "type_based"),
518 (self.context, "context"),
519 ];
520
521 components
522 .iter()
523 .max_by(|a, b| a.0.partial_cmp(&b.0).unwrap_or(std::cmp::Ordering::Equal))
524 .map(|(_, name)| *name)
525 .unwrap_or("unknown")
526 }
527}
528
529pub fn compute_chain_matrix(
531 estimator: &ChainLinkEstimator,
532 links: &[ChainLink],
533) -> Vec<Vec<f32>> {
534 let n = links.len();
535 let mut matrix = vec![vec![0.0; n]; n];
536
537 for i in 0..n {
538 for j in 0..n {
539 if i == j {
540 matrix[i][j] = 1.0; } else {
542 matrix[i][j] = estimator.estimate(&links[i], &links[j]);
543 }
544 }
545 }
546
547 matrix
548}
549
550pub fn find_strongest_links(
552 estimator: &ChainLinkEstimator,
553 links: &[ChainLink],
554) -> Vec<Option<(usize, f32)>> {
555 let n = links.len();
556 let mut strongest = vec![None; n];
557
558 for i in 0..n {
559 let mut best: Option<(usize, f32)> = None;
560
561 for j in 0..n {
562 if i == j {
563 continue;
564 }
565
566 let strength = estimator.estimate(&links[i], &links[j]);
567
568 match best {
569 Some((_, best_strength)) if strength > best_strength => {
570 best = Some((j, strength));
571 }
572 None => {
573 best = Some((j, strength));
574 }
575 _ => {}
576 }
577 }
578
579 strongest[i] = best;
580 }
581
582 strongest
583}
584
585#[cfg(test)]
586mod tests {
587 use super::*;
588
589 fn make_test_link(
590 depth: u32,
591 temporal: f32,
592 embedding_seed: f32,
593 link_type: LinkType,
594 ) -> ChainLink {
595 let coord = TrajectoryCoordinate5D::new(depth, 0, 0.8, temporal, 1);
596 let embedding: Vec<f32> = (0..8)
597 .map(|i| (embedding_seed + i as f32 * 0.1).sin())
598 .collect();
599
600 ChainLink::new(coord, embedding, link_type)
601 }
602
603 #[test]
604 fn test_link_type_parsing() {
605 assert_eq!(LinkType::from_str("continuation"), LinkType::Continuation);
606 assert_eq!(LinkType::from_str("QUESTION"), LinkType::Question);
607 assert_eq!(LinkType::from_str("code"), LinkType::Code);
608 assert_eq!(LinkType::from_str("unknown_type"), LinkType::Unknown);
609 }
610
611 #[test]
612 fn test_link_type_properties() {
613 assert!(LinkType::Continuation.is_constructive());
614 assert!(LinkType::Answer.is_constructive());
615 assert!(!LinkType::Question.is_constructive());
616
617 assert!(LinkType::Question.is_questioning());
618 assert!(LinkType::Error.is_questioning());
619 assert!(!LinkType::Answer.is_questioning());
620 }
621
622 #[test]
623 fn test_chain_link_creation() {
624 let coord = TrajectoryCoordinate5D::new(2, 0, 0.9, 0.5, 1);
625 let embedding = vec![0.5; 8];
626 let link = ChainLink::new(coord, embedding, LinkType::Continuation);
627
628 assert!((link.influence - 0.5).abs() < 1e-6);
629 assert!(!link.is_user);
630 assert!(link.attention.is_none());
631 }
632
633 #[test]
634 fn test_chain_link_similarity() {
635 let link_a = make_test_link(1, 0.2, 1.0, LinkType::Continuation);
636 let link_b = make_test_link(2, 0.4, 1.1, LinkType::Elaboration);
637
638 let sim = link_a.semantic_similarity(&link_b);
639 assert!(sim > 0.9); let link_c = make_test_link(3, 0.6, 5.0, LinkType::Tangent);
642 let sim_c = link_a.semantic_similarity(&link_c);
643 assert!(sim_c < sim); }
645
646 #[test]
647 fn test_estimator_config_default() {
648 let config = ChainLinkEstimatorConfig::default();
649
650 let total = config.baseline_weight
652 + config.relationship_weight
653 + config.type_weight
654 + config.context_weight;
655 assert!((total - 1.0).abs() < 1e-6);
656 }
657
658 #[test]
659 fn test_estimator_type_compatibility() {
660 let config = ChainLinkEstimatorConfig::default();
661
662 let qa_compat = config.get_type_compatibility(LinkType::Question, LinkType::Answer);
664 assert!(qa_compat > 0.8);
665
666 let es_compat = config.get_type_compatibility(LinkType::Error, LinkType::Solution);
668 assert!(es_compat > 0.8);
669
670 let ct_compat = config.get_type_compatibility(LinkType::Continuation, LinkType::Tangent);
672 assert!(ct_compat < 0.5);
673 }
674
675 #[test]
676 fn test_estimator_estimate() {
677 let estimator = ChainLinkEstimator::default();
678
679 let link_a = make_test_link(1, 0.2, 1.0, LinkType::Question);
680 let link_b = make_test_link(2, 0.4, 1.1, LinkType::Answer);
681
682 let strength = estimator.estimate(&link_a, &link_b);
683
684 assert!(strength > 0.0);
686 assert!(strength <= 1.0);
687 }
688
689 #[test]
690 fn test_estimator_self_similarity() {
691 let estimator = ChainLinkEstimator::default();
692
693 let link = make_test_link(2, 0.5, 1.0, LinkType::Continuation);
694 let strength = estimator.estimate(&link, &link);
695
696 assert!(strength > 0.8);
698 }
699
700 #[test]
701 fn test_estimator_detailed() {
702 let estimator = ChainLinkEstimator::default();
703
704 let link_a = make_test_link(1, 0.2, 1.0, LinkType::Error);
705 let link_b = make_test_link(2, 0.4, 1.2, LinkType::Solution);
706
707 let estimate = estimator.estimate_detailed(&link_a, &link_b);
708
709 assert!(estimate.total > 0.0);
710 assert!(estimate.baseline > 0.0);
711 assert!(estimate.relationship > 0.0);
712 assert!(estimate.type_based > 0.0);
713 assert!(estimate.context > 0.0);
714
715 assert!(estimate.type_based > 0.8);
717 }
718
719 #[test]
720 fn test_estimate_dominant_component() {
721 let estimate = ChainLinkEstimate {
722 total: 0.75,
723 baseline: 0.9,
724 relationship: 0.6,
725 type_based: 0.7,
726 context: 0.5,
727 };
728
729 assert_eq!(estimate.dominant_component(), "baseline");
730 assert!(estimate.is_strong());
731 assert!(!estimate.is_weak());
732 }
733
734 #[test]
735 fn test_compute_chain_matrix() {
736 let estimator = ChainLinkEstimator::default();
737
738 let links = vec![
739 make_test_link(1, 0.1, 1.0, LinkType::Question),
740 make_test_link(2, 0.3, 1.2, LinkType::Answer),
741 make_test_link(3, 0.5, 1.4, LinkType::Elaboration),
742 ];
743
744 let matrix = compute_chain_matrix(&estimator, &links);
745
746 assert_eq!(matrix.len(), 3);
747 assert_eq!(matrix[0].len(), 3);
748
749 assert!((matrix[0][0] - 1.0).abs() < 1e-6);
751 assert!((matrix[1][1] - 1.0).abs() < 1e-6);
752 assert!((matrix[2][2] - 1.0).abs() < 1e-6);
753
754 }
757
758 #[test]
759 fn test_find_strongest_links() {
760 let estimator = ChainLinkEstimator::default();
761
762 let links = vec![
763 make_test_link(1, 0.1, 1.0, LinkType::Question),
764 make_test_link(2, 0.3, 1.1, LinkType::Answer), make_test_link(3, 0.8, 5.0, LinkType::Tangent), ];
767
768 let strongest = find_strongest_links(&estimator, &links);
769
770 assert_eq!(strongest.len(), 3);
771
772 assert!(strongest[0].is_some());
774
775 for (i, s) in strongest.iter().enumerate() {
777 assert!(s.is_some(), "Link {} should have a strongest connection", i);
778 }
779 }
780
781 #[test]
782 fn test_chain_link_with_metadata() {
783 let coord = TrajectoryCoordinate5D::new(2, 0, 0.9, 0.5, 1);
784 let embedding = vec![0.5; 8];
785 let link = ChainLink::with_metadata(
786 coord,
787 embedding,
788 LinkType::Continuation,
789 0.8,
790 true, );
792
793 assert!((link.influence - 0.8).abs() < 1e-6);
794 assert!(link.is_user);
795 }
796
797 #[test]
798 fn test_chain_link_fluent_api() {
799 let coord = TrajectoryCoordinate5D::new(2, 0, 0.9, 0.5, 1);
800 let embedding = vec![0.5; 8];
801 let attention = AttentionWeights::uniform(3);
802
803 let link = ChainLink::new(coord, embedding, LinkType::Continuation)
804 .with_attention(attention)
805 .with_id("test_link_1");
806
807 assert!(link.attention.is_some());
808 assert_eq!(link.id, Some("test_link_1".to_string()));
809 }
810}