1#![allow(clippy::manual_clamp)]
2
3use legalis_core::{Condition, Statute};
47use serde::{Deserialize, Serialize};
48use thiserror::Error;
49
50pub mod adaptive;
51pub mod advanced_cache;
52pub mod advanced_visual;
53pub mod algorithms;
54pub mod analysis;
55pub mod api;
56pub mod audit;
57pub mod cloud;
58pub mod collaborative;
59pub mod collaborative_review;
60pub mod compliance;
61pub mod cross_jurisdiction;
62pub mod distributed;
63pub mod dsl;
64pub mod enterprise;
65pub mod export;
66pub mod export_plugins;
67pub mod formats;
68pub mod fuzzy;
69pub mod git;
70pub mod gpu;
71pub mod integration;
72pub mod integration_examples;
73pub mod legal_domain;
74pub mod legislative_history;
75pub mod llm;
76pub mod machine_readable;
77pub mod merge;
78pub mod ml;
79pub mod ml_advanced;
80pub mod multilingual;
81pub mod nlp;
82pub mod optimization;
83pub mod parallel;
84pub mod patterns;
85pub mod plugins;
86pub mod quantum;
87pub mod realtime;
88pub mod recommendation;
89pub mod rollback;
90pub mod scripting;
91pub mod security;
92pub mod semantic;
93pub mod simd;
94pub mod statistics;
95pub mod streaming;
96pub mod templates;
97pub mod time_travel;
98pub mod timeline;
99pub mod timeseries;
100pub mod validation;
101pub mod vcs;
102pub mod vcs_integration;
103pub mod visual;
104
105#[derive(Debug, Error)]
107pub enum DiffError {
108 #[error("Cannot compare statutes with different IDs: {0} vs {1}")]
109 IdMismatch(String, String),
110
111 #[error("Invalid comparison: {0}")]
112 InvalidComparison(String),
113
114 #[error("Empty statute provided: {0}")]
115 EmptyStatute(String),
116
117 #[error("Version conflict: {old_version} -> {new_version}")]
118 VersionConflict { old_version: u32, new_version: u32 },
119
120 #[error("Merge conflict detected at {location}: {description}")]
121 MergeConflict {
122 location: String,
123 description: String,
124 },
125
126 #[error("Unsupported operation: {0}")]
127 UnsupportedOperation(String),
128
129 #[error("Serialization error: {0}")]
130 SerializationError(String),
131}
132
133pub type DiffResult<T> = Result<T, DiffError>;
135
136#[derive(Debug, Clone, Serialize, Deserialize)]
138pub struct StatuteDiff {
139 pub statute_id: String,
141 pub version_info: Option<VersionInfo>,
143 pub changes: Vec<Change>,
145 pub impact: ImpactAssessment,
147}
148
149#[derive(Debug, Clone, Serialize, Deserialize)]
151pub struct VersionInfo {
152 pub old_version: Option<u32>,
153 pub new_version: Option<u32>,
154}
155
156#[derive(Debug, Clone, Serialize, Deserialize)]
158pub struct Change {
159 pub change_type: ChangeType,
161 pub target: ChangeTarget,
163 pub description: String,
165 pub old_value: Option<String>,
167 pub new_value: Option<String>,
169}
170
171#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Serialize, Deserialize)]
173pub enum ChangeType {
174 Added,
176 Removed,
178 Modified,
180 Reordered,
182}
183
184#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
186pub enum ChangeTarget {
187 Title,
188 Precondition { index: usize },
189 Effect,
190 DiscretionLogic,
191 Metadata { key: String },
192}
193
194impl std::fmt::Display for ChangeTarget {
195 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
196 match self {
197 Self::Title => write!(f, "Title"),
198 Self::Precondition { index } => write!(f, "Precondition #{}", index + 1),
199 Self::Effect => write!(f, "Effect"),
200 Self::DiscretionLogic => write!(f, "Discretion Logic"),
201 Self::Metadata { key } => write!(f, "Metadata[{}]", key),
202 }
203 }
204}
205
206#[derive(Debug, Clone, Default, Serialize, Deserialize)]
208pub struct ImpactAssessment {
209 pub severity: Severity,
211 pub affects_eligibility: bool,
213 pub affects_outcome: bool,
215 pub discretion_changed: bool,
217 pub notes: Vec<String>,
219}
220
221#[derive(
223 Debug, Clone, Copy, Default, PartialEq, Eq, PartialOrd, Ord, Hash, Serialize, Deserialize,
224)]
225pub enum Severity {
226 #[default]
228 None,
229 Minor,
231 Moderate,
233 Major,
235 Breaking,
237}
238
239pub fn diff(old: &Statute, new: &Statute) -> DiffResult<StatuteDiff> {
268 if old.id != new.id {
269 return Err(DiffError::IdMismatch(old.id.clone(), new.id.clone()));
270 }
271
272 let mut changes = Vec::new();
273 let mut impact = ImpactAssessment::default();
274
275 if old.title != new.title {
277 changes.push(Change {
278 change_type: ChangeType::Modified,
279 target: ChangeTarget::Title,
280 description: "Title was modified".to_string(),
281 old_value: Some(old.title.clone()),
282 new_value: Some(new.title.clone()),
283 });
284 impact.severity = impact.severity.max(Severity::Minor);
285 }
286
287 diff_preconditions(
289 &old.preconditions,
290 &new.preconditions,
291 &mut changes,
292 &mut impact,
293 );
294
295 if old.effect != new.effect {
297 changes.push(Change {
298 change_type: ChangeType::Modified,
299 target: ChangeTarget::Effect,
300 description: "Effect was modified".to_string(),
301 old_value: Some(format!("{:?}", old.effect)),
302 new_value: Some(format!("{:?}", new.effect)),
303 });
304 impact.affects_outcome = true;
305 impact.severity = impact.severity.max(Severity::Major);
306 impact
307 .notes
308 .push("Outcome of statute application changed".to_string());
309 }
310
311 match (&old.discretion_logic, &new.discretion_logic) {
313 (None, Some(logic)) => {
314 changes.push(Change {
315 change_type: ChangeType::Added,
316 target: ChangeTarget::DiscretionLogic,
317 description: "Discretion logic was added".to_string(),
318 old_value: None,
319 new_value: Some(logic.clone()),
320 });
321 impact.discretion_changed = true;
322 impact.severity = impact.severity.max(Severity::Major);
323 impact.notes.push("Human judgment now required".to_string());
324 }
325 (Some(old_logic), None) => {
326 changes.push(Change {
327 change_type: ChangeType::Removed,
328 target: ChangeTarget::DiscretionLogic,
329 description: "Discretion logic was removed".to_string(),
330 old_value: Some(old_logic.clone()),
331 new_value: None,
332 });
333 impact.discretion_changed = true;
334 impact.severity = impact.severity.max(Severity::Major);
335 impact
336 .notes
337 .push("Human judgment no longer required - now deterministic".to_string());
338 }
339 (Some(old_logic), Some(new_logic)) if old_logic != new_logic => {
340 changes.push(Change {
341 change_type: ChangeType::Modified,
342 target: ChangeTarget::DiscretionLogic,
343 description: "Discretion logic was modified".to_string(),
344 old_value: Some(old_logic.clone()),
345 new_value: Some(new_logic.clone()),
346 });
347 impact.discretion_changed = true;
348 impact.severity = impact.severity.max(Severity::Moderate);
349 }
350 _ => {}
351 }
352
353 Ok(StatuteDiff {
354 statute_id: old.id.clone(),
355 version_info: None,
356 changes,
357 impact,
358 })
359}
360
361fn diff_preconditions(
362 old: &[Condition],
363 new: &[Condition],
364 changes: &mut Vec<Change>,
365 impact: &mut ImpactAssessment,
366) {
367 let old_len = old.len();
368 let new_len = new.len();
369
370 if new_len > old_len {
372 for (i, cond) in new.iter().enumerate().skip(old_len) {
373 changes.push(Change {
374 change_type: ChangeType::Added,
375 target: ChangeTarget::Precondition { index: i },
376 description: format!("New precondition added at position {}", i + 1),
377 old_value: None,
378 new_value: Some(format!("{:?}", cond)),
379 });
380 }
381 impact.affects_eligibility = true;
382 impact.severity = impact.severity.max(Severity::Major);
383 impact
384 .notes
385 .push("New eligibility conditions added".to_string());
386 } else if old_len > new_len {
387 for (i, cond) in old.iter().enumerate().skip(new_len) {
388 changes.push(Change {
389 change_type: ChangeType::Removed,
390 target: ChangeTarget::Precondition { index: i },
391 description: format!("Precondition removed from position {}", i + 1),
392 old_value: Some(format!("{:?}", cond)),
393 new_value: None,
394 });
395 }
396 impact.affects_eligibility = true;
397 impact.severity = impact.severity.max(Severity::Major);
398 impact
399 .notes
400 .push("Eligibility conditions removed".to_string());
401 }
402
403 for i in 0..old_len.min(new_len) {
405 if old[i] != new[i] {
406 changes.push(Change {
407 change_type: ChangeType::Modified,
408 target: ChangeTarget::Precondition { index: i },
409 description: format!("Precondition {} was modified", i + 1),
410 old_value: Some(format!("{:?}", old[i])),
411 new_value: Some(format!("{:?}", new[i])),
412 });
413 impact.affects_eligibility = true;
414 impact.severity = impact.severity.max(Severity::Moderate);
415 }
416 }
417}
418
419pub fn summarize(diff: &StatuteDiff) -> String {
445 let mut summary = format!("Diff for statute '{}'\n", diff.statute_id);
446 summary.push_str(&format!("Severity: {:?}\n", diff.impact.severity));
447 summary.push_str(&format!("Changes: {}\n\n", diff.changes.len()));
448
449 for change in &diff.changes {
450 summary.push_str(&format!(
451 " [{:?}] {}: {}\n",
452 change.change_type, change.target, change.description
453 ));
454 }
455
456 if !diff.impact.notes.is_empty() {
457 summary.push_str("\nImpact Notes:\n");
458 for note in &diff.impact.notes {
459 summary.push_str(&format!(" - {}\n", note));
460 }
461 }
462
463 summary
464}
465
466pub fn filter_changes_by_type(diff: &StatuteDiff, change_type: ChangeType) -> Vec<Change> {
487 diff.changes
488 .iter()
489 .filter(|c| c.change_type == change_type)
490 .cloned()
491 .collect()
492}
493
494pub fn has_breaking_changes(diff: &StatuteDiff) -> bool {
515 use crate::Severity;
516
517 diff.impact.severity >= Severity::Major
518 || diff.impact.affects_outcome
519 || diff.impact.discretion_changed
520}
521
522pub fn count_changes_by_target(diff: &StatuteDiff) -> std::collections::HashMap<String, usize> {
540 use std::collections::HashMap;
541
542 let mut counts: HashMap<String, usize> = HashMap::new();
543
544 for change in &diff.changes {
545 let key = match &change.target {
546 ChangeTarget::Title => "Title".to_string(),
547 ChangeTarget::Precondition { .. } => "Precondition".to_string(),
548 ChangeTarget::Effect => "Effect".to_string(),
549 ChangeTarget::DiscretionLogic => "DiscretionLogic".to_string(),
550 ChangeTarget::Metadata { .. } => "Metadata".to_string(),
551 };
552 *counts.entry(key).or_insert(0) += 1;
553 }
554
555 counts
556}
557
558pub fn diff_sequence(versions: &[Statute]) -> DiffResult<Vec<StatuteDiff>> {
584 if versions.len() < 2 {
585 return Ok(Vec::new());
586 }
587
588 let mut diffs = Vec::new();
589 for i in 0..versions.len() - 1 {
590 diffs.push(diff(&versions[i], &versions[i + 1])?);
591 }
592
593 Ok(diffs)
594}
595
596#[derive(Debug, Clone, Serialize, Deserialize)]
598pub struct DetailedSummary {
599 pub statute_id: String,
601 pub overall_confidence: f64,
603 pub change_count: usize,
605 pub severity: Severity,
607 pub summary_text: String,
609 pub change_detection_confidence: f64,
611 pub impact_assessment_confidence: f64,
613 pub insights: Vec<String>,
615}
616
617pub fn detailed_summary(diff: &StatuteDiff) -> DetailedSummary {
639 let mut insights = Vec::new();
640 let change_count = diff.changes.len();
641
642 let change_detection_confidence = if change_count == 0 {
644 1.0 } else {
646 0.95 };
648
649 let impact_assessment_confidence = if diff.impact.affects_outcome
651 || diff.impact.affects_eligibility
652 || diff.impact.discretion_changed
653 {
654 0.9 } else if diff.impact.severity >= Severity::Moderate {
656 0.85
657 } else if diff.impact.severity == Severity::Minor {
658 0.8
659 } else {
660 0.95 };
662
663 if diff.impact.affects_eligibility {
665 insights
666 .push("This change affects who is eligible for the statute's provisions.".to_string());
667 }
668
669 if diff.impact.affects_outcome {
670 insights.push("This change modifies the outcome or effect of the statute.".to_string());
671 }
672
673 if diff.impact.discretion_changed {
674 insights.push("Discretionary judgment requirements have been modified.".to_string());
675 }
676
677 let added_count = diff
679 .changes
680 .iter()
681 .filter(|c| c.change_type == ChangeType::Added)
682 .count();
683 let removed_count = diff
684 .changes
685 .iter()
686 .filter(|c| c.change_type == ChangeType::Removed)
687 .count();
688 let modified_count = diff
689 .changes
690 .iter()
691 .filter(|c| c.change_type == ChangeType::Modified)
692 .count();
693
694 if added_count > 0 {
695 insights.push(format!("{} new element(s) added.", added_count));
696 }
697 if removed_count > 0 {
698 insights.push(format!("{} element(s) removed.", removed_count));
699 }
700 if modified_count > 0 {
701 insights.push(format!("{} element(s) modified.", modified_count));
702 }
703
704 let overall_confidence = (change_detection_confidence + impact_assessment_confidence) / 2.0;
706
707 let summary_text = summarize(diff);
709
710 DetailedSummary {
711 statute_id: diff.statute_id.clone(),
712 overall_confidence,
713 change_count,
714 severity: diff.impact.severity,
715 summary_text,
716 change_detection_confidence,
717 impact_assessment_confidence,
718 insights,
719 }
720}
721
722pub fn diff_preconditions_only(old: &Statute, new: &Statute) -> DiffResult<Vec<Change>> {
753 if old.id != new.id {
754 return Err(DiffError::IdMismatch(old.id.clone(), new.id.clone()));
755 }
756
757 let mut changes = Vec::new();
758 let mut impact = ImpactAssessment::default();
759
760 diff_preconditions(
761 &old.preconditions,
762 &new.preconditions,
763 &mut changes,
764 &mut impact,
765 );
766
767 Ok(changes)
768}
769
770pub fn diff_effect_only(old: &Statute, new: &Statute) -> DiffResult<Option<Change>> {
789 if old.id != new.id {
790 return Err(DiffError::IdMismatch(old.id.clone(), new.id.clone()));
791 }
792
793 if old.effect != new.effect {
794 Ok(Some(Change {
795 change_type: ChangeType::Modified,
796 target: ChangeTarget::Effect,
797 description: "Effect was modified".to_string(),
798 old_value: Some(format!("{:?}", old.effect)),
799 new_value: Some(format!("{:?}", new.effect)),
800 }))
801 } else {
802 Ok(None)
803 }
804}
805
806#[cfg(test)]
807mod tests {
808 use super::*;
809 use legalis_core::{ComparisonOp, Effect, EffectType};
810
811 fn test_statute() -> Statute {
812 Statute::new(
813 "test-statute",
814 "Test Statute",
815 Effect::new(EffectType::Grant, "Test effect"),
816 )
817 .with_precondition(Condition::Age {
818 operator: ComparisonOp::GreaterOrEqual,
819 value: 18,
820 })
821 }
822
823 fn empty_statute() -> Statute {
824 Statute::new(
825 "empty-statute",
826 "Empty Statute",
827 Effect::new(EffectType::Grant, "Empty effect"),
828 )
829 }
830
831 #[test]
832 fn test_no_changes() {
833 let statute = test_statute();
834 let result = diff(&statute, &statute).unwrap();
835 assert!(result.changes.is_empty());
836 assert_eq!(result.impact.severity, Severity::None);
837 }
838
839 #[test]
840 fn test_identical_statutes() {
841 let statute1 = test_statute();
842 let statute2 = test_statute();
843 let result = diff(&statute1, &statute2).unwrap();
844 assert!(result.changes.is_empty());
845 assert_eq!(result.impact.severity, Severity::None);
846 assert!(!result.impact.affects_eligibility);
847 assert!(!result.impact.affects_outcome);
848 }
849
850 #[test]
851 fn test_empty_statutes() {
852 let statute1 = empty_statute();
853 let statute2 = empty_statute();
854 let result = diff(&statute1, &statute2).unwrap();
855 assert!(result.changes.is_empty());
856 assert_eq!(result.impact.severity, Severity::None);
857 }
858
859 #[test]
860 fn test_empty_to_populated() {
861 let old = empty_statute();
862 let mut new = old.clone();
863 new.preconditions.push(Condition::Age {
864 operator: ComparisonOp::GreaterOrEqual,
865 value: 18,
866 });
867
868 let result = diff(&old, &new).unwrap();
869 assert_eq!(result.changes.len(), 1);
870 assert!(result.impact.affects_eligibility);
871 assert!(matches!(result.changes[0].change_type, ChangeType::Added));
872 }
873
874 #[test]
875 fn test_title_change() {
876 let old = test_statute();
877 let mut new = old.clone();
878 new.title = "Modified Title".to_string();
879
880 let result = diff(&old, &new).unwrap();
881 assert_eq!(result.changes.len(), 1);
882 assert!(matches!(result.changes[0].target, ChangeTarget::Title));
883 }
884
885 #[test]
886 fn test_precondition_added() {
887 let old = test_statute();
888 let mut new = old.clone();
889 new.preconditions.push(Condition::Income {
890 operator: ComparisonOp::LessOrEqual,
891 value: 5000000,
892 });
893
894 let result = diff(&old, &new).unwrap();
895 assert!(result.impact.affects_eligibility);
896 assert!(result.impact.severity >= Severity::Major);
897 }
898
899 #[test]
900 fn test_precondition_removed() {
901 let mut old = test_statute();
902 old.preconditions.push(Condition::Income {
903 operator: ComparisonOp::LessOrEqual,
904 value: 5000000,
905 });
906 let new = test_statute(); let result = diff(&old, &new).unwrap();
909 assert!(result.impact.affects_eligibility);
910 assert!(result.impact.severity >= Severity::Major);
911 let has_removed = result
912 .changes
913 .iter()
914 .any(|c| matches!(c.change_type, ChangeType::Removed));
915 assert!(has_removed);
916 }
917
918 #[test]
919 fn test_precondition_modified() {
920 let old = test_statute();
921 let mut new = old.clone();
922 new.preconditions[0] = Condition::Age {
923 operator: ComparisonOp::GreaterOrEqual,
924 value: 21,
925 };
926
927 let result = diff(&old, &new).unwrap();
928 assert!(result.impact.affects_eligibility);
929 assert!(!result.changes.is_empty());
930 let has_modified = result
931 .changes
932 .iter()
933 .any(|c| matches!(c.change_type, ChangeType::Modified));
934 assert!(has_modified);
935 }
936
937 #[test]
938 fn test_effect_change() {
939 let old = test_statute();
940 let mut new = old.clone();
941 new.effect = Effect::new(EffectType::Revoke, "Revoke instead");
942
943 let result = diff(&old, &new).unwrap();
944 assert!(result.impact.affects_outcome);
945 assert_eq!(result.impact.severity, Severity::Major);
946 }
947
948 #[test]
949 fn test_discretion_added() {
950 let old = test_statute();
951 let new = old
952 .clone()
953 .with_discretion("Consider special circumstances");
954
955 let result = diff(&old, &new).unwrap();
956 assert!(result.impact.discretion_changed);
957 }
958
959 #[test]
960 fn test_discretion_removed() {
961 let old = test_statute().with_discretion("Consider special circumstances");
962 let new = test_statute();
963
964 let result = diff(&old, &new).unwrap();
965 assert!(result.impact.discretion_changed);
966 assert!(result.impact.severity >= Severity::Major);
967 }
968
969 #[test]
970 fn test_discretion_modified() {
971 let old = test_statute().with_discretion("Consider special circumstances");
972 let new = test_statute().with_discretion("Consider different circumstances");
973
974 let result = diff(&old, &new).unwrap();
975 assert!(result.impact.discretion_changed);
976 }
977
978 #[test]
979 fn test_multiple_changes() {
980 let old = test_statute();
981 let mut new = old.clone();
982 new.title = "New Title".to_string();
983 new.preconditions.push(Condition::Income {
984 operator: ComparisonOp::LessOrEqual,
985 value: 5000000,
986 });
987 new.effect = Effect::new(EffectType::Obligation, "New effect");
988
989 let result = diff(&old, &new).unwrap();
990 assert!(result.changes.len() >= 3);
991 assert!(result.impact.affects_eligibility);
992 assert!(result.impact.affects_outcome);
993 assert_eq!(result.impact.severity, Severity::Major);
994 }
995
996 #[test]
997 fn test_id_mismatch_error() {
998 let old = test_statute();
999 let mut new = test_statute();
1000 new.id = "different-id".to_string();
1001
1002 let result = diff(&old, &new);
1003 assert!(matches!(result, Err(DiffError::IdMismatch(_, _))));
1004 }
1005
1006 #[test]
1007 fn test_summarize() {
1008 let old = test_statute();
1009 let mut new = old.clone();
1010 new.title = "Modified Title".to_string();
1011
1012 let result = diff(&old, &new).unwrap();
1013 let summary = summarize(&result);
1014
1015 assert!(summary.contains("test-statute"));
1016 assert!(summary.contains("Modified"));
1017 }
1018
1019 #[test]
1020 fn test_all_preconditions_removed() {
1021 let old = test_statute();
1022 let mut new = old.clone();
1023 new.preconditions.clear();
1024
1025 let result = diff(&old, &new).unwrap();
1026 assert!(result.impact.affects_eligibility);
1027 assert!(result.impact.severity >= Severity::Major);
1028 }
1029
1030 #[test]
1031 fn test_version_info_present() {
1032 let old = test_statute();
1033 let new = test_statute();
1034
1035 let mut result = diff(&old, &new).unwrap();
1036 result.version_info = Some(VersionInfo {
1037 old_version: Some(1),
1038 new_version: Some(2),
1039 });
1040
1041 assert!(result.version_info.is_some());
1042 assert_eq!(result.version_info.as_ref().unwrap().old_version, Some(1));
1043 assert_eq!(result.version_info.as_ref().unwrap().new_version, Some(2));
1044 }
1045
1046 #[test]
1047 fn test_detailed_summary() {
1048 let old = test_statute();
1049 let mut new = old.clone();
1050 new.title = "Modified Title".to_string();
1051
1052 let diff_result = diff(&old, &new).unwrap();
1053 let summary = detailed_summary(&diff_result);
1054
1055 assert_eq!(summary.statute_id, "test-statute");
1056 assert!(summary.overall_confidence > 0.0);
1057 assert!(summary.overall_confidence <= 1.0);
1058 assert_eq!(summary.change_count, 1);
1059 assert!(!summary.insights.is_empty());
1060 }
1061
1062 #[test]
1063 fn test_diff_preconditions_only() {
1064 let old = test_statute();
1065 let mut new = old.clone();
1066 new.preconditions[0] = Condition::Age {
1067 operator: ComparisonOp::GreaterOrEqual,
1068 value: 21,
1069 };
1070
1071 let changes = diff_preconditions_only(&old, &new).unwrap();
1072 assert!(!changes.is_empty());
1073 assert!(matches!(
1074 changes[0].target,
1075 ChangeTarget::Precondition { .. }
1076 ));
1077 }
1078
1079 #[test]
1080 fn test_diff_effect_only() {
1081 let old = test_statute();
1082 let mut new = old.clone();
1083 new.effect = Effect::new(EffectType::Revoke, "Different effect");
1084
1085 let change = diff_effect_only(&old, &new).unwrap();
1086 assert!(change.is_some());
1087 let c = change.unwrap();
1088 assert!(matches!(c.target, ChangeTarget::Effect));
1089 }
1090
1091 #[test]
1092 fn test_diff_effect_only_no_change() {
1093 let old = test_statute();
1094 let new = old.clone();
1095
1096 let change = diff_effect_only(&old, &new).unwrap();
1097 assert!(change.is_none());
1098 }
1099}