1pub mod case_law;
286pub mod const_collections;
287pub mod formats;
288pub mod testing;
289pub mod transactions;
290pub mod typed_attributes;
291pub mod typed_effects;
292pub mod workflows;
293
294pub mod arena;
296pub mod compact;
297pub mod interning;
298pub mod lazy;
299pub mod parallel_eval;
300
301pub mod distributed;
303
304pub mod formal_methods;
306
307pub mod knowledge_graph;
309
310pub mod temporal;
312
313pub mod document_processing;
315
316pub mod probabilistic;
318
319pub mod multi_jurisdictional;
321
322pub mod explanation;
324
325pub mod rule_learning;
327
328pub mod adaptive_cache;
330pub mod gpu_offload;
331pub mod jit_framework;
332pub mod memory_pool;
333pub mod simd_eval;
334
335pub mod explainable_ai;
337pub mod finetuned_llm;
338pub mod hybrid_reasoning;
339pub mod llm_interpretation;
340pub mod neural_entailment;
341
342pub mod blockchain_verification;
344pub mod decentralized_registry;
345pub mod oracle;
346pub mod smart_contract;
347pub mod zkp;
348
349pub mod digital_twin;
351pub mod event_sourcing;
352pub mod realtime_sync;
353pub mod scenario_simulation;
354pub mod time_travel;
355
356pub mod quantum;
358
359pub mod autonomous_agents;
361
362use chrono::{DateTime, Datelike, NaiveDate, Utc};
363use std::collections::HashMap;
364use std::fmt;
365use uuid::Uuid;
366
367#[cfg(feature = "serde")]
368use serde::{Deserialize, Serialize};
369
370#[cfg(feature = "schema")]
371use schemars::JsonSchema;
372
373pub use case_law::{
375 Case, CaseDatabase, CaseRule, Court, DamageType, Precedent, PrecedentApplication,
376 PrecedentWeight,
377};
378
379pub use typed_attributes::{AttributeError, AttributeValue, TypedAttributes};
381
382#[derive(Debug, Clone, PartialEq)]
431#[cfg_attr(feature = "serde", derive(Serialize, Deserialize))]
432#[cfg_attr(feature = "schema", derive(JsonSchema))]
433pub enum LegalResult<T> {
434 Deterministic(T),
437
438 JudicialDiscretion {
443 issue: String,
445 context_id: Uuid,
447 narrative_hint: Option<String>,
449 },
450
451 Void { reason: String },
453}
454
455impl<T> LegalResult<T> {
456 pub fn is_deterministic(&self) -> bool {
458 matches!(self, Self::Deterministic(_))
459 }
460
461 pub fn requires_discretion(&self) -> bool {
463 matches!(self, Self::JudicialDiscretion { .. })
464 }
465
466 pub fn is_void(&self) -> bool {
468 matches!(self, Self::Void { .. })
469 }
470
471 pub fn map<U, F: FnOnce(T) -> U>(self, f: F) -> LegalResult<U> {
473 match self {
474 Self::Deterministic(t) => LegalResult::Deterministic(f(t)),
475 Self::JudicialDiscretion {
476 issue,
477 context_id,
478 narrative_hint,
479 } => LegalResult::JudicialDiscretion {
480 issue,
481 context_id,
482 narrative_hint,
483 },
484 Self::Void { reason } => LegalResult::Void { reason },
485 }
486 }
487}
488
489impl<T: fmt::Display> fmt::Display for LegalResult<T> {
490 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
491 match self {
492 Self::Deterministic(value) => write!(f, "Deterministic({})", value),
493 Self::JudicialDiscretion {
494 issue,
495 narrative_hint,
496 ..
497 } => {
498 write!(f, "JudicialDiscretion: {}", issue)?;
499 if let Some(hint) = narrative_hint {
500 write!(f, " [hint: {}]", hint)?;
501 }
502 Ok(())
503 }
504 Self::Void { reason } => write!(f, "Void: {}", reason),
505 }
506 }
507}
508
509pub trait LegalEntity: Send + Sync {
529 fn id(&self) -> Uuid;
531
532 fn get_attribute(&self, key: &str) -> Option<String>;
534
535 fn set_attribute(&mut self, key: &str, value: String);
537}
538
539#[derive(Debug, Clone)]
556#[cfg_attr(feature = "serde", derive(Serialize, Deserialize))]
557#[cfg_attr(feature = "schema", derive(JsonSchema))]
558pub struct BasicEntity {
559 id: Uuid,
560 attributes: std::collections::HashMap<String, String>,
561}
562
563impl BasicEntity {
564 pub fn new() -> Self {
566 Self {
567 id: Uuid::new_v4(),
568 attributes: std::collections::HashMap::new(),
569 }
570 }
571
572 pub fn with_id(id: Uuid) -> Self {
574 Self {
575 id,
576 attributes: std::collections::HashMap::new(),
577 }
578 }
579}
580
581impl Default for BasicEntity {
582 fn default() -> Self {
583 Self::new()
584 }
585}
586
587impl LegalEntity for BasicEntity {
588 fn id(&self) -> Uuid {
589 self.id
590 }
591
592 fn get_attribute(&self, key: &str) -> Option<String> {
593 self.attributes.get(key).cloned()
594 }
595
596 fn set_attribute(&mut self, key: &str, value: String) {
597 self.attributes.insert(key.to_string(), value);
598 }
599}
600
601#[derive(Debug, Clone)]
626#[cfg_attr(feature = "serde", derive(Serialize, Deserialize))]
627#[cfg_attr(feature = "schema", derive(JsonSchema))]
628pub struct TypedEntity {
629 id: Uuid,
630 attributes: TypedAttributes,
631}
632
633impl TypedEntity {
634 pub fn new() -> Self {
636 Self {
637 id: Uuid::new_v4(),
638 attributes: TypedAttributes::new(),
639 }
640 }
641
642 pub fn with_id(id: Uuid) -> Self {
644 Self {
645 id,
646 attributes: TypedAttributes::new(),
647 }
648 }
649
650 pub fn attributes(&self) -> &TypedAttributes {
652 &self.attributes
653 }
654
655 pub fn attributes_mut(&mut self) -> &mut TypedAttributes {
657 &mut self.attributes
658 }
659
660 pub fn set_u32(&mut self, key: impl Into<String>, value: u32) {
662 self.attributes.set_u32(key, value);
663 }
664
665 pub fn get_u32(&self, key: &str) -> Result<u32, AttributeError> {
667 self.attributes.get_u32(key)
668 }
669
670 pub fn set_u64(&mut self, key: impl Into<String>, value: u64) {
672 self.attributes.set_u64(key, value);
673 }
674
675 pub fn get_u64(&self, key: &str) -> Result<u64, AttributeError> {
677 self.attributes.get_u64(key)
678 }
679
680 pub fn set_bool(&mut self, key: impl Into<String>, value: bool) {
682 self.attributes.set_bool(key, value);
683 }
684
685 pub fn get_bool(&self, key: &str) -> Result<bool, AttributeError> {
687 self.attributes.get_bool(key)
688 }
689
690 pub fn set_string(&mut self, key: impl Into<String>, value: impl Into<String>) {
692 self.attributes.set_string(key, value);
693 }
694
695 pub fn get_string(&self, key: &str) -> Result<&str, AttributeError> {
697 self.attributes.get_string(key)
698 }
699
700 pub fn set_date(&mut self, key: impl Into<String>, value: NaiveDate) {
702 self.attributes.set_date(key, value);
703 }
704
705 pub fn get_date(&self, key: &str) -> Result<NaiveDate, AttributeError> {
707 self.attributes.get_date(key)
708 }
709
710 pub fn set_f64(&mut self, key: impl Into<String>, value: f64) {
712 self.attributes.set_f64(key, value);
713 }
714
715 pub fn get_f64(&self, key: &str) -> Result<f64, AttributeError> {
717 self.attributes.get_f64(key)
718 }
719
720 pub fn set_typed(&mut self, key: impl Into<String>, value: AttributeValue) {
722 self.attributes.set(key, value);
723 }
724
725 pub fn get_typed(&self, key: &str) -> Option<&AttributeValue> {
727 self.attributes.get(key)
728 }
729
730 pub fn has_attribute(&self, key: &str) -> bool {
732 self.attributes.has(key)
733 }
734}
735
736impl Default for TypedEntity {
737 fn default() -> Self {
738 Self::new()
739 }
740}
741
742impl LegalEntity for TypedEntity {
743 fn id(&self) -> Uuid {
744 self.id
745 }
746
747 fn get_attribute(&self, key: &str) -> Option<String> {
748 self.attributes.get(key).map(|v| v.to_string_value())
749 }
750
751 fn set_attribute(&mut self, key: &str, value: String) {
752 self.attributes
753 .set(key, AttributeValue::parse_from_string(&value));
754 }
755}
756
757#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Hash)]
759#[cfg_attr(feature = "serde", derive(Serialize, Deserialize))]
760#[cfg_attr(feature = "schema", derive(JsonSchema))]
761pub enum DurationUnit {
762 Days,
764 Weeks,
766 Months,
768 Years,
770}
771
772impl fmt::Display for DurationUnit {
773 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
774 match self {
775 Self::Days => write!(f, "days"),
776 Self::Weeks => write!(f, "weeks"),
777 Self::Months => write!(f, "months"),
778 Self::Years => write!(f, "years"),
779 }
780 }
781}
782
783#[derive(Debug, Clone, PartialEq)]
806#[cfg_attr(feature = "serde", derive(Serialize, Deserialize))]
807#[cfg_attr(feature = "schema", derive(JsonSchema))]
808pub enum PartialBool {
809 True {
811 confidence: f64,
813 reason: String,
815 },
816 False {
818 confidence: f64,
820 reason: String,
822 },
823 Unknown {
825 confidence: f64,
827 reason: String,
829 },
830}
831
832impl PartialBool {
833 #[must_use]
835 pub fn true_with_confidence(confidence: f64) -> Self {
836 Self::True {
837 confidence,
838 reason: String::new(),
839 }
840 }
841
842 #[must_use]
844 pub fn true_with_confidence_and_reason(confidence: f64, reason: &str) -> Self {
845 Self::True {
846 confidence,
847 reason: reason.to_string(),
848 }
849 }
850
851 #[must_use]
853 pub fn false_with_confidence(confidence: f64) -> Self {
854 Self::False {
855 confidence,
856 reason: String::new(),
857 }
858 }
859
860 #[must_use]
862 pub fn false_with_confidence_and_reason(confidence: f64, reason: &str) -> Self {
863 Self::False {
864 confidence,
865 reason: reason.to_string(),
866 }
867 }
868
869 #[must_use]
871 pub fn unknown(confidence: f64, reason: &str) -> Self {
872 Self::Unknown {
873 confidence,
874 reason: reason.to_string(),
875 }
876 }
877
878 #[must_use]
880 pub fn confidence(&self) -> f64 {
881 match self {
882 Self::True { confidence, .. }
883 | Self::False { confidence, .. }
884 | Self::Unknown { confidence, .. } => *confidence,
885 }
886 }
887
888 #[must_use]
890 pub fn reason(&self) -> &str {
891 match self {
892 Self::True { reason, .. }
893 | Self::False { reason, .. }
894 | Self::Unknown { reason, .. } => reason,
895 }
896 }
897
898 #[must_use]
900 pub fn is_true(&self) -> bool {
901 matches!(self, Self::True { .. })
902 }
903
904 #[must_use]
906 pub fn is_false(&self) -> bool {
907 matches!(self, Self::False { .. })
908 }
909
910 #[must_use]
912 pub fn is_unknown(&self) -> bool {
913 matches!(self, Self::Unknown { .. })
914 }
915}
916
917impl fmt::Display for PartialBool {
918 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
919 match self {
920 Self::True { confidence, reason } => {
921 if reason.is_empty() {
922 write!(f, "True (confidence: {:.2})", confidence)
923 } else {
924 write!(
925 f,
926 "True (confidence: {:.2}, reason: {})",
927 confidence, reason
928 )
929 }
930 }
931 Self::False { confidence, reason } => {
932 if reason.is_empty() {
933 write!(f, "False (confidence: {:.2})", confidence)
934 } else {
935 write!(
936 f,
937 "False (confidence: {:.2}, reason: {})",
938 confidence, reason
939 )
940 }
941 }
942 Self::Unknown { confidence, reason } => {
943 write!(
944 f,
945 "Unknown (confidence: {:.2}, reason: {})",
946 confidence, reason
947 )
948 }
949 }
950 }
951}
952
953#[derive(Debug, Clone)]
977#[cfg_attr(feature = "serde", derive(Serialize, Deserialize))]
978pub struct EvaluationExplanation {
979 pub condition: String,
981 pub conclusion: bool,
983 pub steps: Vec<ExplanationStep>,
985}
986
987impl fmt::Display for EvaluationExplanation {
988 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
989 writeln!(f, "Evaluation of: {}", self.condition)?;
990 writeln!(f, "Result: {}", self.conclusion)?;
991 writeln!(f, "\nEvaluation trace:")?;
992 for (i, step) in self.steps.iter().enumerate() {
993 let indent = " ".repeat(step.depth);
994 writeln!(
995 f,
996 "{}{}. {} -> {} ({}μs)",
997 indent,
998 i + 1,
999 step.condition,
1000 step.result,
1001 step.duration_micros
1002 )?;
1003 if !step.details.is_empty() {
1004 writeln!(f, "{} Details: {}", indent, step.details)?;
1005 }
1006 }
1007 Ok(())
1008 }
1009}
1010
1011#[derive(Debug, Clone)]
1015#[cfg_attr(feature = "serde", derive(Serialize, Deserialize))]
1016pub struct ExplanationStep {
1017 pub condition: String,
1019 pub result: bool,
1021 pub details: String,
1023 pub depth: usize,
1025 pub duration_micros: u64,
1027}
1028
1029#[derive(Debug, Clone, PartialEq)]
1070#[cfg_attr(feature = "serde", derive(Serialize, Deserialize))]
1071#[cfg_attr(feature = "schema", derive(JsonSchema))]
1072pub enum Condition {
1073 Age { operator: ComparisonOp, value: u32 },
1075 Income { operator: ComparisonOp, value: u64 },
1077 HasAttribute { key: String },
1079 AttributeEquals { key: String, value: String },
1081 DateRange {
1083 start: Option<NaiveDate>,
1084 end: Option<NaiveDate>,
1085 },
1086 Geographic {
1088 region_type: RegionType,
1089 region_id: String,
1090 },
1091 EntityRelationship {
1093 relationship_type: RelationshipType,
1094 target_entity_id: Option<String>,
1095 },
1096 ResidencyDuration { operator: ComparisonOp, months: u32 },
1098 Duration {
1100 operator: ComparisonOp,
1101 value: u32,
1102 unit: DurationUnit,
1103 },
1104 Percentage {
1106 operator: ComparisonOp,
1107 value: u32,
1108 context: String,
1109 },
1110 SetMembership {
1112 attribute: String,
1113 values: Vec<String>,
1114 negated: bool,
1115 },
1116 Pattern {
1118 attribute: String,
1119 pattern: String,
1120 negated: bool,
1121 },
1122 Calculation {
1125 formula: String,
1126 operator: ComparisonOp,
1127 value: f64,
1128 },
1129 Composite {
1132 conditions: Vec<(f64, Box<Condition>)>,
1135 threshold: f64,
1137 },
1138 Threshold {
1141 attributes: Vec<(String, f64)>,
1143 operator: ComparisonOp,
1145 value: f64,
1147 },
1148 Fuzzy {
1151 attribute: String,
1153 membership_points: Vec<(f64, f64)>,
1156 min_membership: f64,
1158 },
1159 Probabilistic {
1162 condition: Box<Condition>,
1164 probability: f64,
1167 threshold: f64,
1169 },
1170 Temporal {
1173 base_value: f64,
1175 reference_time: i64,
1177 rate: f64,
1180 operator: ComparisonOp,
1182 target_value: f64,
1184 },
1185 And(Box<Condition>, Box<Condition>),
1187 Or(Box<Condition>, Box<Condition>),
1189 Not(Box<Condition>),
1191 Custom { description: String },
1193}
1194
1195impl Condition {
1196 #[must_use]
1198 pub const fn is_compound(&self) -> bool {
1199 matches!(self, Self::And(..) | Self::Or(..) | Self::Not(..))
1200 }
1201
1202 #[must_use]
1204 pub const fn is_simple(&self) -> bool {
1205 !self.is_compound()
1206 }
1207
1208 #[must_use]
1210 pub const fn is_negation(&self) -> bool {
1211 matches!(self, Self::Not(..))
1212 }
1213
1214 #[must_use]
1216 pub fn count_conditions(&self) -> usize {
1217 match self {
1218 Self::And(left, right) | Self::Or(left, right) => {
1219 1 + left.count_conditions() + right.count_conditions()
1220 }
1221 Self::Not(inner) => 1 + inner.count_conditions(),
1222 Self::Composite { conditions, .. } => {
1223 1 + conditions
1224 .iter()
1225 .map(|(_, c)| c.count_conditions())
1226 .sum::<usize>()
1227 }
1228 Self::Probabilistic { condition, .. } => 1 + condition.count_conditions(),
1229 _ => 1,
1230 }
1231 }
1232
1233 #[must_use]
1235 pub fn depth(&self) -> usize {
1236 match self {
1237 Self::And(left, right) | Self::Or(left, right) => 1 + left.depth().max(right.depth()),
1238 Self::Not(inner) => 1 + inner.depth(),
1239 Self::Composite { conditions, .. } => {
1240 1 + conditions.iter().map(|(_, c)| c.depth()).max().unwrap_or(0)
1241 }
1242 Self::Probabilistic { condition, .. } => 1 + condition.depth(),
1243 _ => 1,
1244 }
1245 }
1246
1247 pub fn age(operator: ComparisonOp, value: u32) -> Self {
1249 Self::Age { operator, value }
1250 }
1251
1252 pub fn income(operator: ComparisonOp, value: u64) -> Self {
1254 Self::Income { operator, value }
1255 }
1256
1257 pub fn has_attribute(key: impl Into<String>) -> Self {
1259 Self::HasAttribute { key: key.into() }
1260 }
1261
1262 pub fn attribute_equals(key: impl Into<String>, value: impl Into<String>) -> Self {
1264 Self::AttributeEquals {
1265 key: key.into(),
1266 value: value.into(),
1267 }
1268 }
1269
1270 pub fn custom(description: impl Into<String>) -> Self {
1272 Self::Custom {
1273 description: description.into(),
1274 }
1275 }
1276
1277 pub fn duration(operator: ComparisonOp, value: u32, unit: DurationUnit) -> Self {
1279 Self::Duration {
1280 operator,
1281 value,
1282 unit,
1283 }
1284 }
1285
1286 pub fn percentage(operator: ComparisonOp, value: u32, context: impl Into<String>) -> Self {
1288 Self::Percentage {
1289 operator,
1290 value,
1291 context: context.into(),
1292 }
1293 }
1294
1295 pub fn in_set(attribute: impl Into<String>, values: Vec<String>) -> Self {
1297 Self::SetMembership {
1298 attribute: attribute.into(),
1299 values,
1300 negated: false,
1301 }
1302 }
1303
1304 pub fn not_in_set(attribute: impl Into<String>, values: Vec<String>) -> Self {
1306 Self::SetMembership {
1307 attribute: attribute.into(),
1308 values,
1309 negated: true,
1310 }
1311 }
1312
1313 pub fn matches_pattern(attribute: impl Into<String>, pattern: impl Into<String>) -> Self {
1315 Self::Pattern {
1316 attribute: attribute.into(),
1317 pattern: pattern.into(),
1318 negated: false,
1319 }
1320 }
1321
1322 pub fn not_matches_pattern(attribute: impl Into<String>, pattern: impl Into<String>) -> Self {
1324 Self::Pattern {
1325 attribute: attribute.into(),
1326 pattern: pattern.into(),
1327 negated: true,
1328 }
1329 }
1330
1331 pub fn calculation(formula: impl Into<String>, operator: ComparisonOp, value: f64) -> Self {
1340 Self::Calculation {
1341 formula: formula.into(),
1342 operator,
1343 value,
1344 }
1345 }
1346
1347 pub fn composite(conditions: Vec<(f64, Box<Condition>)>, threshold: f64) -> Self {
1365 Self::Composite {
1366 conditions,
1367 threshold,
1368 }
1369 }
1370
1371 pub fn threshold(attributes: Vec<(String, f64)>, operator: ComparisonOp, value: f64) -> Self {
1389 Self::Threshold {
1390 attributes,
1391 operator,
1392 value,
1393 }
1394 }
1395
1396 pub fn fuzzy(
1414 attribute: String,
1415 membership_points: Vec<(f64, f64)>,
1416 min_membership: f64,
1417 ) -> Self {
1418 Self::Fuzzy {
1419 attribute,
1420 membership_points,
1421 min_membership,
1422 }
1423 }
1424
1425 pub fn probabilistic(condition: Box<Condition>, probability: f64, threshold: f64) -> Self {
1443 Self::Probabilistic {
1444 condition,
1445 probability,
1446 threshold,
1447 }
1448 }
1449
1450 pub fn temporal(
1472 base_value: f64,
1473 reference_time: i64,
1474 rate: f64,
1475 operator: ComparisonOp,
1476 target_value: f64,
1477 ) -> Self {
1478 Self::Temporal {
1479 base_value,
1480 reference_time,
1481 rate,
1482 operator,
1483 target_value,
1484 }
1485 }
1486
1487 pub fn and(self, other: Condition) -> Self {
1489 Self::And(Box::new(self), Box::new(other))
1490 }
1491
1492 pub fn or(self, other: Condition) -> Self {
1494 Self::Or(Box::new(self), Box::new(other))
1495 }
1496
1497 #[allow(clippy::should_implement_trait)]
1499 pub fn not(self) -> Self {
1500 Self::Not(Box::new(self))
1501 }
1502
1503 #[must_use]
1521 pub fn normalize(self) -> Self {
1522 match self {
1523 Self::Not(inner) => match *inner {
1525 Self::Not(double_inner) => double_inner.normalize(),
1526 Self::And(left, right) => {
1528 Self::Or(
1530 Box::new(Self::Not(left).normalize()),
1531 Box::new(Self::Not(right).normalize()),
1532 )
1533 }
1534 Self::Or(left, right) => {
1535 Self::And(
1537 Box::new(Self::Not(left).normalize()),
1538 Box::new(Self::Not(right).normalize()),
1539 )
1540 }
1541 other => Self::Not(Box::new(other.normalize())),
1542 },
1543 Self::And(left, right) => {
1545 Self::And(Box::new(left.normalize()), Box::new(right.normalize()))
1546 }
1547 Self::Or(left, right) => {
1548 Self::Or(Box::new(left.normalize()), Box::new(right.normalize()))
1549 }
1550 other => other,
1552 }
1553 }
1554
1555 #[must_use]
1557 pub fn is_normalized(&self) -> bool {
1558 match self {
1559 Self::Not(inner) => !matches!(**inner, Self::Not(_)) && inner.is_normalized(),
1561 Self::And(left, right) | Self::Or(left, right) => {
1562 left.is_normalized() && right.is_normalized()
1563 }
1564 _ => true,
1565 }
1566 }
1567
1568 pub fn evaluate_simple(&self, ctx: &AttributeBasedContext) -> Result<bool, ConditionError> {
1604 self.evaluate_simple_with_depth(ctx, 0)
1605 }
1606
1607 fn evaluate_simple_with_depth(
1609 &self,
1610 ctx: &AttributeBasedContext,
1611 depth: usize,
1612 ) -> Result<bool, ConditionError> {
1613 if depth > ctx.max_depth {
1615 return Err(ConditionError::MaxDepthExceeded {
1616 max_depth: ctx.max_depth,
1617 });
1618 }
1619
1620 match self {
1621 Self::And(left, right) => {
1623 let left_result = left.evaluate_simple_with_depth(ctx, depth + 1)?;
1624 if !left_result {
1625 return Ok(false);
1627 }
1628 right.evaluate_simple_with_depth(ctx, depth + 1)
1630 }
1631 Self::Or(left, right) => {
1633 let left_result = left.evaluate_simple_with_depth(ctx, depth + 1)?;
1634 if left_result {
1635 return Ok(true);
1637 }
1638 right.evaluate_simple_with_depth(ctx, depth + 1)
1640 }
1641 Self::Not(inner) => {
1642 let result = inner.evaluate_simple_with_depth(ctx, depth + 1)?;
1643 Ok(!result)
1644 }
1645 Self::Age { operator, value } => {
1646 let age_str =
1647 ctx.attributes
1648 .get("age")
1649 .ok_or_else(|| ConditionError::MissingAttribute {
1650 key: "age".to_string(),
1651 })?;
1652 let age: u32 = age_str.parse().map_err(|_| ConditionError::TypeMismatch {
1653 expected: "u32".to_string(),
1654 actual: age_str.clone(),
1655 })?;
1656 Ok(operator.compare_u32(age, *value))
1657 }
1658 Self::Income { operator, value } => {
1659 let income_str = ctx.attributes.get("income").ok_or_else(|| {
1660 ConditionError::MissingAttribute {
1661 key: "income".to_string(),
1662 }
1663 })?;
1664 let income: u64 = income_str
1665 .parse()
1666 .map_err(|_| ConditionError::TypeMismatch {
1667 expected: "u64".to_string(),
1668 actual: income_str.clone(),
1669 })?;
1670 Ok(operator.compare_u64(income, *value))
1671 }
1672 Self::HasAttribute { key } => Ok(ctx.attributes.contains_key(key)),
1673 Self::AttributeEquals { key, value } => Ok(ctx.attributes.get(key) == Some(value)),
1674 Self::Calculation {
1675 formula,
1676 operator,
1677 value,
1678 } => {
1679 let result = Self::evaluate_formula(formula, ctx)?;
1681 Ok(operator.compare_f64(result, *value))
1682 }
1683 Self::Pattern {
1684 attribute,
1685 pattern,
1686 negated,
1687 } => {
1688 let attr_value = ctx.attributes.get(attribute).ok_or_else(|| {
1689 ConditionError::MissingAttribute {
1690 key: attribute.clone(),
1691 }
1692 })?;
1693 let matches = attr_value.contains(pattern);
1695 Ok(if *negated { !matches } else { matches })
1696 }
1697 Self::ResidencyDuration { operator, months } => {
1698 let residency_str = ctx.attributes.get("residency_months").ok_or_else(|| {
1699 ConditionError::MissingAttribute {
1700 key: "residency_months".to_string(),
1701 }
1702 })?;
1703 let residency: u32 =
1704 residency_str
1705 .parse()
1706 .map_err(|_| ConditionError::TypeMismatch {
1707 expected: "u32".to_string(),
1708 actual: residency_str.clone(),
1709 })?;
1710 Ok(operator.compare_u32(residency, *months))
1711 }
1712 _ => Ok(true),
1715 }
1716 }
1717
1718 #[allow(dead_code)]
1721 fn evaluate_formula(
1722 formula: &str,
1723 _ctx: &AttributeBasedContext,
1724 ) -> Result<f64, ConditionError> {
1725 Err(ConditionError::InvalidFormula {
1730 formula: formula.to_string(),
1731 error: "Formula evaluation not yet implemented - consider using 'meval' or 'evalexpr' crate".to_string(),
1732 })
1733 }
1734
1735 pub fn evaluate<C: EvaluationContext>(&self, context: &C) -> Result<bool, EvaluationError> {
1771 self.evaluate_with_depth(context, 0)
1772 }
1773
1774 fn evaluate_with_depth<C: EvaluationContext>(
1776 &self,
1777 context: &C,
1778 depth: usize,
1779 ) -> Result<bool, EvaluationError> {
1780 const MAX_DEPTH: usize = 100;
1781
1782 if depth > MAX_DEPTH {
1784 return Err(EvaluationError::MaxDepthExceeded {
1785 max_depth: MAX_DEPTH,
1786 });
1787 }
1788
1789 match self {
1790 Self::And(left, right) => {
1792 let left_result = left.evaluate_with_depth(context, depth + 1)?;
1793 if !left_result {
1794 return Ok(false);
1795 }
1796 right.evaluate_with_depth(context, depth + 1)
1797 }
1798 Self::Or(left, right) => {
1800 let left_result = left.evaluate_with_depth(context, depth + 1)?;
1801 if left_result {
1802 return Ok(true);
1803 }
1804 right.evaluate_with_depth(context, depth + 1)
1805 }
1806 Self::Not(inner) => {
1807 let result = inner.evaluate_with_depth(context, depth + 1)?;
1808 Ok(!result)
1809 }
1810 Self::Age { operator, value } => {
1811 let age = context
1812 .get_age()
1813 .ok_or_else(|| EvaluationError::MissingAttribute {
1814 key: "age".to_string(),
1815 })?;
1816 Ok(operator.compare_u32(age, *value))
1817 }
1818 Self::Income { operator, value } => {
1819 let income =
1820 context
1821 .get_income()
1822 .ok_or_else(|| EvaluationError::MissingAttribute {
1823 key: "income".to_string(),
1824 })?;
1825 Ok(operator.compare_u64(income, *value))
1826 }
1827 Self::HasAttribute { key } => Ok(context.get_attribute(key).is_some()),
1828 Self::AttributeEquals { key, value } => {
1829 Ok(context.get_attribute(key).as_ref() == Some(value))
1830 }
1831 Self::Geographic {
1832 region_type,
1833 region_id,
1834 } => Ok(context.check_geographic(*region_type, region_id)),
1835 Self::EntityRelationship {
1836 relationship_type,
1837 target_entity_id,
1838 } => Ok(context.check_relationship(*relationship_type, target_entity_id.as_deref())),
1839 Self::ResidencyDuration { operator, months } => {
1840 let residency = context.get_residency_months().ok_or_else(|| {
1841 EvaluationError::MissingContext {
1842 description: "residency months".to_string(),
1843 }
1844 })?;
1845 Ok(operator.compare_u32(residency, *months))
1846 }
1847 Self::Duration {
1848 operator,
1849 value,
1850 unit,
1851 } => {
1852 let duration =
1853 context
1854 .get_duration(*unit)
1855 .ok_or_else(|| EvaluationError::MissingContext {
1856 description: format!("duration for unit {:?}", unit),
1857 })?;
1858 Ok(operator.compare_u32(duration, *value))
1859 }
1860 Self::Percentage {
1861 operator,
1862 value,
1863 context: pct_context,
1864 } => {
1865 let percentage = context.get_percentage(pct_context).ok_or_else(|| {
1866 EvaluationError::MissingContext {
1867 description: format!("percentage for context '{}'", pct_context),
1868 }
1869 })?;
1870 Ok(operator.compare_u32(percentage, *value))
1871 }
1872 Self::Calculation {
1873 formula,
1874 operator,
1875 value,
1876 } => {
1877 let result = context.evaluate_formula(formula).ok_or_else(|| {
1878 EvaluationError::InvalidFormula {
1879 formula: formula.clone(),
1880 reason: "Formula evaluation not supported".to_string(),
1881 }
1882 })?;
1883 Ok(operator.compare_f64(result, *value))
1884 }
1885 Self::Pattern {
1886 attribute,
1887 pattern,
1888 negated,
1889 } => {
1890 let attr_value = context.get_attribute(attribute).ok_or_else(|| {
1891 EvaluationError::MissingAttribute {
1892 key: attribute.clone(),
1893 }
1894 })?;
1895 let matches = attr_value.contains(pattern);
1896 Ok(if *negated { !matches } else { matches })
1897 }
1898 Self::SetMembership {
1899 attribute,
1900 values,
1901 negated,
1902 } => {
1903 let attr_value = context.get_attribute(attribute).ok_or_else(|| {
1904 EvaluationError::MissingAttribute {
1905 key: attribute.clone(),
1906 }
1907 })?;
1908 let is_member = values.contains(&attr_value);
1909 Ok(if *negated { !is_member } else { is_member })
1910 }
1911 Self::DateRange { start, end } => {
1912 let current_date =
1913 context
1914 .get_current_date()
1915 .ok_or_else(|| EvaluationError::MissingContext {
1916 description: "current date".to_string(),
1917 })?;
1918 let after_start = start.is_none_or(|s| current_date >= s);
1919 let before_end = end.is_none_or(|e| current_date <= e);
1920 Ok(after_start && before_end)
1921 }
1922 Self::Composite {
1923 conditions,
1924 threshold,
1925 } => {
1926 let mut total_score = 0.0;
1927 for (weight, condition) in conditions {
1928 let satisfied = condition.evaluate_with_depth(context, depth + 1)?;
1929 if satisfied {
1930 total_score += weight;
1931 }
1932 }
1933 Ok(total_score >= *threshold)
1934 }
1935 Self::Threshold {
1936 attributes,
1937 operator,
1938 value,
1939 } => {
1940 let mut total = 0.0;
1941 for (attr_name, multiplier) in attributes {
1942 let attr_value = context
1944 .get_attribute(attr_name)
1945 .and_then(|s| s.parse::<f64>().ok())
1946 .ok_or_else(|| EvaluationError::MissingAttribute {
1947 key: attr_name.clone(),
1948 })?;
1949 total += attr_value * multiplier;
1950 }
1951 Ok(operator.compare_f64(total, *value))
1952 }
1953 Self::Fuzzy {
1954 attribute,
1955 membership_points,
1956 min_membership,
1957 } => {
1958 let attr_value = context
1960 .get_attribute(attribute)
1961 .and_then(|s| s.parse::<f64>().ok())
1962 .ok_or_else(|| EvaluationError::MissingAttribute {
1963 key: attribute.clone(),
1964 })?;
1965
1966 let membership = if membership_points.is_empty() {
1968 0.0
1969 } else if membership_points.len() == 1 {
1970 membership_points[0].1
1971 } else {
1972 let mut sorted = membership_points.clone();
1974 sorted
1975 .sort_by(|a, b| a.0.partial_cmp(&b.0).unwrap_or(std::cmp::Ordering::Equal));
1976
1977 if attr_value <= sorted[0].0 {
1979 sorted[0].1
1980 } else if attr_value >= sorted[sorted.len() - 1].0 {
1981 sorted[sorted.len() - 1].1
1982 } else {
1983 let mut result = 0.0;
1985 for i in 0..sorted.len() - 1 {
1986 if attr_value >= sorted[i].0 && attr_value <= sorted[i + 1].0 {
1987 let (x0, y0) = sorted[i];
1988 let (x1, y1) = sorted[i + 1];
1989 let t = (attr_value - x0) / (x1 - x0);
1990 result = y0 + t * (y1 - y0);
1991 break;
1992 }
1993 }
1994 result
1995 }
1996 };
1997
1998 Ok(membership >= *min_membership)
1999 }
2000 Self::Probabilistic {
2001 condition,
2002 probability,
2003 threshold,
2004 } => {
2005 let satisfied = condition.evaluate_with_depth(context, depth + 1)?;
2007
2008 let effective_probability = if satisfied { *probability } else { 0.0 };
2011 Ok(effective_probability >= *threshold)
2012 }
2013 Self::Temporal {
2014 base_value,
2015 reference_time,
2016 rate,
2017 operator,
2018 target_value,
2019 } => {
2020 let current_time = context.get_current_timestamp().ok_or_else(|| {
2022 EvaluationError::MissingContext {
2023 description: "current timestamp".to_string(),
2024 }
2025 })?;
2026
2027 let time_elapsed =
2030 (current_time - reference_time) as f64 / (365.25 * 24.0 * 3600.0); let current_value = base_value * (1.0 + rate).powf(time_elapsed);
2034
2035 Ok(operator.compare_f64(current_value, *target_value))
2036 }
2037 Self::Custom { description } => Err(EvaluationError::Custom {
2039 message: format!("Cannot evaluate custom condition: {}", description),
2040 }),
2041 }
2042 }
2043
2044 pub fn evaluate_with_explanation<C: EvaluationContext>(
2071 &self,
2072 context: &C,
2073 ) -> Result<(bool, EvaluationExplanation), EvaluationError> {
2074 let mut steps = Vec::new();
2075 let result = self.evaluate_with_explanation_recursive(context, 0, &mut steps)?;
2076
2077 let explanation = EvaluationExplanation {
2078 condition: format!("{}", self),
2079 conclusion: result,
2080 steps,
2081 };
2082
2083 Ok((result, explanation))
2084 }
2085
2086 fn evaluate_with_explanation_recursive<C: EvaluationContext>(
2088 &self,
2089 context: &C,
2090 depth: usize,
2091 steps: &mut Vec<ExplanationStep>,
2092 ) -> Result<bool, EvaluationError> {
2093 const MAX_DEPTH: usize = 100;
2094
2095 if depth > MAX_DEPTH {
2096 return Err(EvaluationError::MaxDepthExceeded {
2097 max_depth: MAX_DEPTH,
2098 });
2099 }
2100
2101 let start_time = std::time::Instant::now();
2102
2103 let (result, details) = match self {
2104 Self::And(left, right) => {
2105 let left_result =
2106 left.evaluate_with_explanation_recursive(context, depth + 1, steps)?;
2107 if !left_result {
2108 (
2109 false,
2110 "AND operation short-circuited (left operand is false)".to_string(),
2111 )
2112 } else {
2113 let right_result =
2114 right.evaluate_with_explanation_recursive(context, depth + 1, steps)?;
2115 (
2116 right_result,
2117 format!("AND: left={}, right={}", left_result, right_result),
2118 )
2119 }
2120 }
2121 Self::Or(left, right) => {
2122 let left_result =
2123 left.evaluate_with_explanation_recursive(context, depth + 1, steps)?;
2124 if left_result {
2125 (
2126 true,
2127 "OR operation short-circuited (left operand is true)".to_string(),
2128 )
2129 } else {
2130 let right_result =
2131 right.evaluate_with_explanation_recursive(context, depth + 1, steps)?;
2132 (
2133 right_result,
2134 format!("OR: left={}, right={}", left_result, right_result),
2135 )
2136 }
2137 }
2138 Self::Not(inner) => {
2139 let inner_result =
2140 inner.evaluate_with_explanation_recursive(context, depth + 1, steps)?;
2141 (
2142 !inner_result,
2143 format!("NOT: inner={} -> {}", inner_result, !inner_result),
2144 )
2145 }
2146 Self::Age { operator, value } => {
2147 let age = context
2148 .get_age()
2149 .ok_or_else(|| EvaluationError::MissingAttribute {
2150 key: "age".to_string(),
2151 })?;
2152 let result = operator.compare_u32(age, *value);
2153 (
2154 result,
2155 format!("Age check: {} {} {} = {}", age, operator, value, result),
2156 )
2157 }
2158 Self::Income { operator, value } => {
2159 let income =
2160 context
2161 .get_income()
2162 .ok_or_else(|| EvaluationError::MissingAttribute {
2163 key: "income".to_string(),
2164 })?;
2165 let result = operator.compare_u64(income, *value);
2166 (
2167 result,
2168 format!(
2169 "Income check: {} {} {} = {}",
2170 income, operator, value, result
2171 ),
2172 )
2173 }
2174 Self::HasAttribute { key } => {
2175 let has_it = context.get_attribute(key).is_some();
2176 (has_it, format!("HasAttribute '{}': {}", key, has_it))
2177 }
2178 Self::AttributeEquals { key, value } => {
2179 let actual = context.get_attribute(key);
2180 let equals = actual.as_ref() == Some(value);
2181 (
2182 equals,
2183 format!(
2184 "AttributeEquals '{}': expected='{}', actual={:?}, result={}",
2185 key, value, actual, equals
2186 ),
2187 )
2188 }
2189 _ => {
2190 let result = self.evaluate_with_depth(context, depth)?;
2192 (
2193 result,
2194 format!("Condition '{}' evaluated to {}", self, result),
2195 )
2196 }
2197 };
2198
2199 let elapsed = start_time.elapsed().as_micros() as u64;
2200
2201 steps.push(ExplanationStep {
2202 condition: format!("{}", self),
2203 result,
2204 details,
2205 depth,
2206 duration_micros: elapsed,
2207 });
2208
2209 Ok(result)
2210 }
2211
2212 pub fn partial_evaluate<C: EvaluationContext>(&self, context: &C) -> PartialBool {
2248 self.partial_evaluate_with_depth(context, 0)
2249 }
2250
2251 fn partial_evaluate_with_depth<C: EvaluationContext>(
2253 &self,
2254 context: &C,
2255 depth: usize,
2256 ) -> PartialBool {
2257 const MAX_DEPTH: usize = 100;
2258
2259 if depth > MAX_DEPTH {
2260 return PartialBool::unknown(0.0, "Maximum depth exceeded");
2261 }
2262
2263 match self {
2264 Self::And(left, right) => {
2266 let left_result = left.partial_evaluate_with_depth(context, depth + 1);
2267 let right_result = right.partial_evaluate_with_depth(context, depth + 1);
2268
2269 match (&left_result, &right_result) {
2270 (PartialBool::False { .. }, _) => left_result, (_, PartialBool::False { .. }) => right_result, (
2273 PartialBool::True { confidence: c1, .. },
2274 PartialBool::True { confidence: c2, .. },
2275 ) => {
2276 PartialBool::true_with_confidence((*c1).min(*c2))
2278 }
2279 (
2280 PartialBool::Unknown {
2281 confidence: c1,
2282 reason: r1,
2283 },
2284 PartialBool::Unknown {
2285 confidence: c2,
2286 reason: r2,
2287 },
2288 ) => {
2289 let combined_confidence = (*c1).min(*c2);
2291 PartialBool::unknown(combined_confidence, &format!("{} AND {}", r1, r2))
2292 }
2293 _ => {
2294 PartialBool::unknown(0.5, "AND with unknown operand")
2296 }
2297 }
2298 }
2299 Self::Or(left, right) => {
2301 let left_result = left.partial_evaluate_with_depth(context, depth + 1);
2302 let right_result = right.partial_evaluate_with_depth(context, depth + 1);
2303
2304 match (&left_result, &right_result) {
2305 (PartialBool::True { .. }, _) => left_result, (_, PartialBool::True { .. }) => right_result, (
2308 PartialBool::False { confidence: c1, .. },
2309 PartialBool::False { confidence: c2, .. },
2310 ) => {
2311 PartialBool::false_with_confidence((*c1).min(*c2))
2313 }
2314 (
2315 PartialBool::Unknown {
2316 confidence: c1,
2317 reason: r1,
2318 },
2319 PartialBool::Unknown {
2320 confidence: c2,
2321 reason: r2,
2322 },
2323 ) => {
2324 let combined_confidence = (*c1).min(*c2);
2326 PartialBool::unknown(combined_confidence, &format!("{} OR {}", r1, r2))
2327 }
2328 _ => {
2329 PartialBool::unknown(0.5, "OR with unknown operand")
2331 }
2332 }
2333 }
2334 Self::Not(inner) => {
2336 let inner_result = inner.partial_evaluate_with_depth(context, depth + 1);
2337 match inner_result {
2338 PartialBool::True { confidence, reason } => {
2339 PartialBool::false_with_confidence_and_reason(
2340 confidence,
2341 &format!("NOT ({})", reason),
2342 )
2343 }
2344 PartialBool::False { confidence, reason } => {
2345 PartialBool::true_with_confidence_and_reason(
2346 confidence,
2347 &format!("NOT ({})", reason),
2348 )
2349 }
2350 PartialBool::Unknown { confidence, reason } => {
2351 PartialBool::unknown(confidence, &format!("NOT ({})", reason))
2352 }
2353 }
2354 }
2355 Self::Age { operator, value } => match context.get_age() {
2357 Some(age) => {
2358 let result = operator.compare_u32(age, *value);
2359 if result {
2360 PartialBool::true_with_confidence(1.0)
2361 } else {
2362 PartialBool::false_with_confidence(1.0)
2363 }
2364 }
2365 None => PartialBool::unknown(0.0, "age attribute missing"),
2366 },
2367 Self::Income { operator, value } => match context.get_income() {
2368 Some(income) => {
2369 let result = operator.compare_u64(income, *value);
2370 if result {
2371 PartialBool::true_with_confidence(1.0)
2372 } else {
2373 PartialBool::false_with_confidence(1.0)
2374 }
2375 }
2376 None => PartialBool::unknown(0.0, "income attribute missing"),
2377 },
2378 Self::HasAttribute { key } => match context.get_attribute(key) {
2379 Some(_) => PartialBool::true_with_confidence(1.0),
2380 None => PartialBool::false_with_confidence(1.0),
2381 },
2382 Self::AttributeEquals { key, value } => match context.get_attribute(key) {
2383 Some(actual) => {
2384 if &actual == value {
2385 PartialBool::true_with_confidence(1.0)
2386 } else {
2387 PartialBool::false_with_confidence(1.0)
2388 }
2389 }
2390 None => PartialBool::unknown(0.0, &format!("attribute '{}' missing", key)),
2391 },
2392 Self::DateRange { start, end } => match context.get_current_date() {
2393 Some(current_date) => {
2394 let after_start = start.is_none_or(|s| current_date >= s);
2395 let before_end = end.is_none_or(|e| current_date <= e);
2396 let result = after_start && before_end;
2397 if result {
2398 PartialBool::true_with_confidence(1.0)
2399 } else {
2400 PartialBool::false_with_confidence(1.0)
2401 }
2402 }
2403 None => PartialBool::unknown(0.0, "current date missing"),
2404 },
2405 _ => match self.evaluate(context) {
2407 Ok(result) => {
2408 if result {
2409 PartialBool::true_with_confidence(1.0)
2410 } else {
2411 PartialBool::false_with_confidence(1.0)
2412 }
2413 }
2414 Err(_) => PartialBool::unknown(0.0, "evaluation failed or data missing"),
2415 },
2416 }
2417 }
2418}
2419
2420#[derive(Debug, Clone)]
2425pub struct AttributeBasedContext {
2426 pub attributes: HashMap<String, String>,
2428 pub max_depth: usize,
2430 pub cache: Option<ConditionCache>,
2432 pub audit_trail: Option<EvaluationAuditTrail>,
2434}
2435
2436impl AttributeBasedContext {
2437 #[must_use]
2439 pub fn new(attributes: HashMap<String, String>) -> Self {
2440 Self {
2441 attributes,
2442 max_depth: 100,
2443 cache: None,
2444 audit_trail: None,
2445 }
2446 }
2447
2448 #[must_use]
2450 pub fn with_max_depth(attributes: HashMap<String, String>, max_depth: usize) -> Self {
2451 Self {
2452 attributes,
2453 max_depth,
2454 cache: None,
2455 audit_trail: None,
2456 }
2457 }
2458
2459 #[must_use]
2461 pub fn with_cache(attributes: HashMap<String, String>) -> Self {
2462 Self {
2463 attributes,
2464 max_depth: 100,
2465 cache: Some(ConditionCache::new()),
2466 audit_trail: None,
2467 }
2468 }
2469
2470 #[must_use]
2472 pub fn with_cache_capacity(
2473 attributes: HashMap<String, String>,
2474 max_depth: usize,
2475 cache_capacity: usize,
2476 ) -> Self {
2477 Self {
2478 attributes,
2479 max_depth,
2480 cache: Some(ConditionCache::with_capacity(cache_capacity)),
2481 audit_trail: None,
2482 }
2483 }
2484
2485 #[must_use]
2487 pub fn with_audit_trail(attributes: HashMap<String, String>) -> Self {
2488 Self {
2489 attributes,
2490 max_depth: 100,
2491 cache: None,
2492 audit_trail: Some(EvaluationAuditTrail::new()),
2493 }
2494 }
2495
2496 pub fn record_evaluation(&mut self, condition: &str, result: bool, duration_micros: u64) {
2498 if let Some(trail) = &mut self.audit_trail {
2499 trail.record(condition.to_string(), result, duration_micros);
2500 }
2501 }
2502}
2503
2504#[derive(Debug, Clone)]
2509pub struct ConditionCache {
2510 cache: HashMap<String, bool>,
2512 max_capacity: usize,
2514 access_order: Vec<String>,
2516}
2517
2518impl ConditionCache {
2519 #[must_use]
2521 pub fn new() -> Self {
2522 Self {
2523 cache: HashMap::new(),
2524 max_capacity: 1000,
2525 access_order: Vec::new(),
2526 }
2527 }
2528
2529 #[must_use]
2531 pub fn with_capacity(capacity: usize) -> Self {
2532 Self {
2533 cache: HashMap::with_capacity(capacity),
2534 max_capacity: capacity,
2535 access_order: Vec::with_capacity(capacity),
2536 }
2537 }
2538
2539 pub fn get(&mut self, condition_key: &str) -> Option<bool> {
2541 if let Some(&result) = self.cache.get(condition_key) {
2542 if let Some(pos) = self.access_order.iter().position(|k| k == condition_key) {
2544 self.access_order.remove(pos);
2545 }
2546 self.access_order.push(condition_key.to_string());
2547 Some(result)
2548 } else {
2549 None
2550 }
2551 }
2552
2553 pub fn insert(&mut self, condition_key: String, result: bool) {
2555 if self.cache.len() >= self.max_capacity
2557 && !self.cache.contains_key(&condition_key)
2558 && let Some(oldest_key) = self.access_order.first().cloned()
2559 {
2560 self.cache.remove(&oldest_key);
2561 self.access_order.remove(0);
2562 }
2563
2564 self.cache.insert(condition_key.clone(), result);
2565 self.access_order.push(condition_key);
2566 }
2567
2568 pub fn clear(&mut self) {
2570 self.cache.clear();
2571 self.access_order.clear();
2572 }
2573
2574 #[must_use]
2576 pub fn len(&self) -> usize {
2577 self.cache.len()
2578 }
2579
2580 #[must_use]
2582 pub fn is_empty(&self) -> bool {
2583 self.cache.is_empty()
2584 }
2585
2586 #[must_use]
2588 pub fn hit_rate(&self) -> f64 {
2589 0.0
2592 }
2593}
2594
2595impl Default for ConditionCache {
2596 fn default() -> Self {
2597 Self::new()
2598 }
2599}
2600
2601#[derive(Debug, Clone)]
2606pub struct EvaluationAuditTrail {
2607 records: Vec<EvaluationRecord>,
2609 max_records: usize,
2611}
2612
2613impl EvaluationAuditTrail {
2614 #[must_use]
2616 pub fn new() -> Self {
2617 Self {
2618 records: Vec::new(),
2619 max_records: 1000,
2620 }
2621 }
2622
2623 #[must_use]
2625 pub fn with_capacity(capacity: usize) -> Self {
2626 Self {
2627 records: Vec::with_capacity(capacity),
2628 max_records: capacity,
2629 }
2630 }
2631
2632 pub fn record(&mut self, condition: String, result: bool, duration_micros: u64) {
2634 if self.records.len() >= self.max_records {
2636 self.records.remove(0);
2637 }
2638
2639 self.records.push(EvaluationRecord {
2640 timestamp: Utc::now(),
2641 condition,
2642 result,
2643 duration_micros,
2644 });
2645 }
2646
2647 #[must_use]
2649 pub fn records(&self) -> &[EvaluationRecord] {
2650 &self.records
2651 }
2652
2653 #[must_use]
2655 pub fn len(&self) -> usize {
2656 self.records.len()
2657 }
2658
2659 #[must_use]
2661 pub fn is_empty(&self) -> bool {
2662 self.records.is_empty()
2663 }
2664
2665 pub fn clear(&mut self) {
2667 self.records.clear();
2668 }
2669
2670 #[must_use]
2672 pub fn average_duration(&self) -> f64 {
2673 if self.records.is_empty() {
2674 return 0.0;
2675 }
2676 let total: u64 = self.records.iter().map(|r| r.duration_micros).sum();
2677 total as f64 / self.records.len() as f64
2678 }
2679
2680 #[must_use]
2682 pub fn slowest_evaluation(&self) -> Option<&EvaluationRecord> {
2683 self.records.iter().max_by_key(|r| r.duration_micros)
2684 }
2685
2686 #[must_use]
2688 pub fn slow_evaluations(&self, threshold_micros: u64) -> Vec<&EvaluationRecord> {
2689 self.records
2690 .iter()
2691 .filter(|r| r.duration_micros > threshold_micros)
2692 .collect()
2693 }
2694}
2695
2696impl Default for EvaluationAuditTrail {
2697 fn default() -> Self {
2698 Self::new()
2699 }
2700}
2701
2702#[derive(Debug, Clone)]
2704#[cfg_attr(feature = "serde", derive(Serialize, Deserialize))]
2705#[cfg_attr(feature = "schema", derive(JsonSchema))]
2706pub struct EvaluationRecord {
2707 pub timestamp: DateTime<Utc>,
2709 pub condition: String,
2711 pub result: bool,
2713 pub duration_micros: u64,
2715}
2716
2717impl fmt::Display for EvaluationRecord {
2718 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
2719 write!(
2720 f,
2721 "[{}] {} = {} ({} μs)",
2722 self.timestamp.format("%Y-%m-%d %H:%M:%S"),
2723 self.condition,
2724 self.result,
2725 self.duration_micros
2726 )
2727 }
2728}
2729
2730impl fmt::Display for Condition {
2731 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
2732 match self {
2733 Self::Age { operator, value } => write!(f, "age {} {}", operator, value),
2734 Self::Income { operator, value } => write!(f, "income {} {}", operator, value),
2735 Self::HasAttribute { key } => write!(f, "has_attribute({})", key),
2736 Self::AttributeEquals { key, value } => write!(f, "{} == \"{}\"", key, value),
2737 Self::DateRange { start, end } => match (start, end) {
2738 (Some(s), Some(e)) => write!(f, "date in [{}, {}]", s, e),
2739 (Some(s), None) => write!(f, "date >= {}", s),
2740 (None, Some(e)) => write!(f, "date <= {}", e),
2741 (None, None) => write!(f, "date (any)"),
2742 },
2743 Self::Geographic {
2744 region_type,
2745 region_id,
2746 } => {
2747 write!(f, "in {:?}({})", region_type, region_id)
2748 }
2749 Self::EntityRelationship {
2750 relationship_type,
2751 target_entity_id,
2752 } => match target_entity_id {
2753 Some(id) => write!(f, "{:?} with {}", relationship_type, id),
2754 None => write!(f, "has {:?}", relationship_type),
2755 },
2756 Self::ResidencyDuration { operator, months } => {
2757 write!(f, "residency {} {} months", operator, months)
2758 }
2759 Self::Duration {
2760 operator,
2761 value,
2762 unit,
2763 } => {
2764 write!(f, "duration {} {} {}", operator, value, unit)
2765 }
2766 Self::Percentage {
2767 operator,
2768 value,
2769 context,
2770 } => {
2771 write!(f, "{} {} {}%", context, operator, value)
2772 }
2773 Self::SetMembership {
2774 attribute,
2775 values,
2776 negated,
2777 } => {
2778 let op = if *negated { "NOT IN" } else { "IN" };
2779 write!(f, "{} {} {{{}}}", attribute, op, values.join(", "))
2780 }
2781 Self::Pattern {
2782 attribute,
2783 pattern,
2784 negated,
2785 } => {
2786 let op = if *negated { "!~" } else { "=~" };
2787 write!(f, "{} {} /{}/", attribute, op, pattern)
2788 }
2789 Self::Calculation {
2790 formula,
2791 operator,
2792 value,
2793 } => {
2794 write!(f, "({}) {} {}", formula, operator, value)
2795 }
2796 Self::Composite {
2797 conditions,
2798 threshold,
2799 } => {
2800 write!(f, "composite[")?;
2801 for (i, (weight, cond)) in conditions.iter().enumerate() {
2802 if i > 0 {
2803 write!(f, ", ")?;
2804 }
2805 write!(f, "{}*{}", weight, cond)?;
2806 }
2807 write!(f, "] >= {}", threshold)
2808 }
2809 Self::Threshold {
2810 attributes,
2811 operator,
2812 value,
2813 } => {
2814 write!(f, "sum[")?;
2815 for (i, (attr, mult)) in attributes.iter().enumerate() {
2816 if i > 0 {
2817 write!(f, " + ")?;
2818 }
2819 if (*mult - 1.0).abs() < f64::EPSILON {
2820 write!(f, "{}", attr)?;
2821 } else {
2822 write!(f, "{}*{}", mult, attr)?;
2823 }
2824 }
2825 write!(f, "] {} {}", operator, value)
2826 }
2827 Self::Fuzzy {
2828 attribute,
2829 membership_points,
2830 min_membership,
2831 } => {
2832 write!(
2833 f,
2834 "fuzzy({}, membership={:?}) >= {}",
2835 attribute, membership_points, min_membership
2836 )
2837 }
2838 Self::Probabilistic {
2839 condition,
2840 probability,
2841 threshold,
2842 } => {
2843 write!(f, "prob({}, p={}) >= {}", condition, probability, threshold)
2844 }
2845 Self::Temporal {
2846 base_value,
2847 reference_time,
2848 rate,
2849 operator,
2850 target_value,
2851 } => {
2852 write!(
2853 f,
2854 "temporal(base={}, t0={}, rate={}) {} {}",
2855 base_value, reference_time, rate, operator, target_value
2856 )
2857 }
2858 Self::And(left, right) => write!(f, "({} AND {})", left, right),
2859 Self::Or(left, right) => write!(f, "({} OR {})", left, right),
2860 Self::Not(inner) => write!(f, "NOT {}", inner),
2861 Self::Custom { description } => write!(f, "custom({})", description),
2862 }
2863 }
2864}
2865
2866#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
2868#[cfg_attr(feature = "serde", derive(Serialize, Deserialize))]
2869#[cfg_attr(feature = "schema", derive(JsonSchema))]
2870pub enum RegionType {
2871 Country,
2873 State,
2875 City,
2877 District,
2879 PostalCode,
2881 Custom,
2883}
2884
2885#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
2887#[cfg_attr(feature = "serde", derive(Serialize, Deserialize))]
2888#[cfg_attr(feature = "schema", derive(JsonSchema))]
2889pub enum RelationshipType {
2890 ParentChild,
2892 Spouse,
2894 Employment,
2896 Guardian,
2898 BusinessOwner,
2900 Contractual,
2902}
2903
2904#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
2920#[cfg_attr(feature = "serde", derive(Serialize, Deserialize))]
2921#[cfg_attr(feature = "schema", derive(JsonSchema))]
2922pub enum ComparisonOp {
2923 Equal,
2924 NotEqual,
2925 GreaterThan,
2926 GreaterOrEqual,
2927 LessThan,
2928 LessOrEqual,
2929}
2930
2931impl ComparisonOp {
2932 #[must_use]
2943 pub const fn inverse(&self) -> Self {
2944 match self {
2945 Self::Equal => Self::NotEqual,
2946 Self::NotEqual => Self::Equal,
2947 Self::GreaterThan => Self::LessOrEqual,
2948 Self::GreaterOrEqual => Self::LessThan,
2949 Self::LessThan => Self::GreaterOrEqual,
2950 Self::LessOrEqual => Self::GreaterThan,
2951 }
2952 }
2953
2954 #[must_use]
2956 pub const fn is_equality(&self) -> bool {
2957 matches!(self, Self::Equal | Self::NotEqual)
2958 }
2959
2960 #[must_use]
2962 pub const fn is_ordering(&self) -> bool {
2963 !self.is_equality()
2964 }
2965
2966 #[must_use]
2968 pub const fn compare_u32(&self, left: u32, right: u32) -> bool {
2969 match self {
2970 Self::Equal => left == right,
2971 Self::NotEqual => left != right,
2972 Self::GreaterThan => left > right,
2973 Self::GreaterOrEqual => left >= right,
2974 Self::LessThan => left < right,
2975 Self::LessOrEqual => left <= right,
2976 }
2977 }
2978
2979 #[must_use]
2981 pub const fn compare_u64(&self, left: u64, right: u64) -> bool {
2982 match self {
2983 Self::Equal => left == right,
2984 Self::NotEqual => left != right,
2985 Self::GreaterThan => left > right,
2986 Self::GreaterOrEqual => left >= right,
2987 Self::LessThan => left < right,
2988 Self::LessOrEqual => left <= right,
2989 }
2990 }
2991
2992 #[must_use]
2994 pub const fn compare_i64(&self, left: i64, right: i64) -> bool {
2995 match self {
2996 Self::Equal => left == right,
2997 Self::NotEqual => left != right,
2998 Self::GreaterThan => left > right,
2999 Self::GreaterOrEqual => left >= right,
3000 Self::LessThan => left < right,
3001 Self::LessOrEqual => left <= right,
3002 }
3003 }
3004
3005 #[must_use]
3007 pub fn compare_f64(&self, left: f64, right: f64) -> bool {
3008 match self {
3009 Self::Equal => (left - right).abs() < f64::EPSILON,
3010 Self::NotEqual => (left - right).abs() >= f64::EPSILON,
3011 Self::GreaterThan => left > right,
3012 Self::GreaterOrEqual => left >= right,
3013 Self::LessThan => left < right,
3014 Self::LessOrEqual => left <= right,
3015 }
3016 }
3017}
3018
3019impl fmt::Display for ComparisonOp {
3020 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
3021 match self {
3022 Self::Equal => write!(f, "=="),
3023 Self::NotEqual => write!(f, "!="),
3024 Self::GreaterThan => write!(f, ">"),
3025 Self::GreaterOrEqual => write!(f, ">="),
3026 Self::LessThan => write!(f, "<"),
3027 Self::LessOrEqual => write!(f, "<="),
3028 }
3029 }
3030}
3031
3032pub trait EvaluationContext {
3066 fn get_attribute(&self, key: &str) -> Option<String>;
3068
3069 fn get_age(&self) -> Option<u32>;
3071
3072 fn get_income(&self) -> Option<u64>;
3074
3075 fn get_current_date(&self) -> Option<NaiveDate>;
3077
3078 fn get_current_timestamp(&self) -> Option<i64> {
3080 None
3081 }
3082
3083 fn check_geographic(&self, region_type: RegionType, region_id: &str) -> bool;
3085
3086 fn check_relationship(
3088 &self,
3089 relationship_type: RelationshipType,
3090 target_id: Option<&str>,
3091 ) -> bool;
3092
3093 fn get_residency_months(&self) -> Option<u32>;
3095
3096 fn get_duration(&self, unit: DurationUnit) -> Option<u32>;
3098
3099 fn get_percentage(&self, context: &str) -> Option<u32>;
3101
3102 fn evaluate_formula(&self, formula: &str) -> Option<f64>;
3104}
3105
3106#[derive(Debug, Clone, PartialEq, Eq)]
3108#[cfg_attr(feature = "serde", derive(Serialize, Deserialize))]
3109#[cfg_attr(feature = "schema", derive(JsonSchema))]
3110pub enum EvaluationError {
3111 MissingAttribute { key: String },
3113 MissingContext { description: String },
3115 InvalidFormula { formula: String, reason: String },
3117 PatternError { pattern: String, reason: String },
3119 MaxDepthExceeded { max_depth: usize },
3121 Custom { message: String },
3123}
3124
3125impl fmt::Display for EvaluationError {
3126 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
3127 match self {
3128 Self::MissingAttribute { key } => write!(f, "Missing attribute: {}", key),
3129 Self::MissingContext { description } => write!(f, "Missing context: {}", description),
3130 Self::InvalidFormula { formula, reason } => {
3131 write!(f, "Invalid formula '{}': {}", formula, reason)
3132 }
3133 Self::PatternError { pattern, reason } => {
3134 write!(f, "Pattern error '{}': {}", pattern, reason)
3135 }
3136 Self::MaxDepthExceeded { max_depth } => {
3137 write!(f, "Maximum evaluation depth {} exceeded", max_depth)
3138 }
3139 Self::Custom { message } => write!(f, "{}", message),
3140 }
3141 }
3142}
3143
3144impl std::error::Error for EvaluationError {}
3145
3146#[derive(Debug)]
3169pub struct DefaultValueContext<'a, C: EvaluationContext> {
3170 inner: &'a C,
3171 defaults: HashMap<String, String>,
3172}
3173
3174impl<'a, C: EvaluationContext> DefaultValueContext<'a, C> {
3175 pub fn new(inner: &'a C, defaults: HashMap<String, String>) -> Self {
3177 Self { inner, defaults }
3178 }
3179
3180 pub fn with_default(mut self, key: impl Into<String>, value: impl Into<String>) -> Self {
3182 self.defaults.insert(key.into(), value.into());
3183 self
3184 }
3185}
3186
3187impl<'a, C: EvaluationContext> EvaluationContext for DefaultValueContext<'a, C> {
3188 fn get_attribute(&self, key: &str) -> Option<String> {
3189 self.inner
3190 .get_attribute(key)
3191 .or_else(|| self.defaults.get(key).cloned())
3192 }
3193
3194 fn get_age(&self) -> Option<u32> {
3195 self.inner
3196 .get_age()
3197 .or_else(|| self.defaults.get("age").and_then(|s| s.parse::<u32>().ok()))
3198 }
3199
3200 fn get_income(&self) -> Option<u64> {
3201 self.inner.get_income().or_else(|| {
3202 self.defaults
3203 .get("income")
3204 .and_then(|s| s.parse::<u64>().ok())
3205 })
3206 }
3207
3208 fn get_current_date(&self) -> Option<NaiveDate> {
3209 self.inner.get_current_date()
3210 }
3211
3212 fn get_current_timestamp(&self) -> Option<i64> {
3213 self.inner.get_current_timestamp()
3214 }
3215
3216 fn check_geographic(&self, region_type: RegionType, region_id: &str) -> bool {
3217 self.inner.check_geographic(region_type, region_id)
3218 }
3219
3220 fn check_relationship(
3221 &self,
3222 relationship_type: RelationshipType,
3223 target_id: Option<&str>,
3224 ) -> bool {
3225 self.inner.check_relationship(relationship_type, target_id)
3226 }
3227
3228 fn get_residency_months(&self) -> Option<u32> {
3229 self.inner.get_residency_months()
3230 }
3231
3232 fn get_duration(&self, unit: DurationUnit) -> Option<u32> {
3233 self.inner.get_duration(unit)
3234 }
3235
3236 fn get_percentage(&self, context: &str) -> Option<u32> {
3237 self.inner.get_percentage(context)
3238 }
3239
3240 fn evaluate_formula(&self, formula: &str) -> Option<f64> {
3241 self.inner.evaluate_formula(formula)
3242 }
3243}
3244
3245#[derive(Debug)]
3270pub struct FallbackContext<'a, C1: EvaluationContext, C2: EvaluationContext> {
3271 primary: &'a C1,
3272 fallback: &'a C2,
3273}
3274
3275impl<'a, C1: EvaluationContext, C2: EvaluationContext> FallbackContext<'a, C1, C2> {
3276 pub fn new(primary: &'a C1, fallback: &'a C2) -> Self {
3278 Self { primary, fallback }
3279 }
3280}
3281
3282impl<'a, C1: EvaluationContext, C2: EvaluationContext> EvaluationContext
3283 for FallbackContext<'a, C1, C2>
3284{
3285 fn get_attribute(&self, key: &str) -> Option<String> {
3286 self.primary
3287 .get_attribute(key)
3288 .or_else(|| self.fallback.get_attribute(key))
3289 }
3290
3291 fn get_age(&self) -> Option<u32> {
3292 self.primary.get_age().or_else(|| self.fallback.get_age())
3293 }
3294
3295 fn get_income(&self) -> Option<u64> {
3296 self.primary
3297 .get_income()
3298 .or_else(|| self.fallback.get_income())
3299 }
3300
3301 fn get_current_date(&self) -> Option<NaiveDate> {
3302 self.primary
3303 .get_current_date()
3304 .or_else(|| self.fallback.get_current_date())
3305 }
3306
3307 fn get_current_timestamp(&self) -> Option<i64> {
3308 self.primary
3309 .get_current_timestamp()
3310 .or_else(|| self.fallback.get_current_timestamp())
3311 }
3312
3313 fn check_geographic(&self, region_type: RegionType, region_id: &str) -> bool {
3314 self.primary.check_geographic(region_type, region_id)
3315 || self.fallback.check_geographic(region_type, region_id)
3316 }
3317
3318 fn check_relationship(
3319 &self,
3320 relationship_type: RelationshipType,
3321 target_id: Option<&str>,
3322 ) -> bool {
3323 self.primary
3324 .check_relationship(relationship_type, target_id)
3325 || self
3326 .fallback
3327 .check_relationship(relationship_type, target_id)
3328 }
3329
3330 fn get_residency_months(&self) -> Option<u32> {
3331 self.primary
3332 .get_residency_months()
3333 .or_else(|| self.fallback.get_residency_months())
3334 }
3335
3336 fn get_duration(&self, unit: DurationUnit) -> Option<u32> {
3337 self.primary
3338 .get_duration(unit)
3339 .or_else(|| self.fallback.get_duration(unit))
3340 }
3341
3342 fn get_percentage(&self, context: &str) -> Option<u32> {
3343 self.primary
3344 .get_percentage(context)
3345 .or_else(|| self.fallback.get_percentage(context))
3346 }
3347
3348 fn evaluate_formula(&self, formula: &str) -> Option<f64> {
3349 self.primary
3350 .evaluate_formula(formula)
3351 .or_else(|| self.fallback.evaluate_formula(formula))
3352 }
3353}
3354
3355impl EvaluationContext for AttributeBasedContext {
3357 fn get_attribute(&self, key: &str) -> Option<String> {
3358 self.attributes.get(key).cloned()
3359 }
3360
3361 fn get_age(&self) -> Option<u32> {
3362 self.attributes.get("age").and_then(|v| v.parse().ok())
3363 }
3364
3365 fn get_income(&self) -> Option<u64> {
3366 self.attributes.get("income").and_then(|v| v.parse().ok())
3367 }
3368
3369 fn get_current_date(&self) -> Option<NaiveDate> {
3370 self.attributes
3371 .get("current_date")
3372 .and_then(|v| NaiveDate::parse_from_str(v, "%Y-%m-%d").ok())
3373 }
3374
3375 fn check_geographic(&self, _region_type: RegionType, region_id: &str) -> bool {
3376 self.attributes
3377 .get("region")
3378 .is_some_and(|v| v == region_id)
3379 }
3380
3381 fn check_relationship(
3382 &self,
3383 _relationship_type: RelationshipType,
3384 target_id: Option<&str>,
3385 ) -> bool {
3386 if let Some(target) = target_id {
3387 self.attributes
3388 .get("relationship")
3389 .is_some_and(|v| v == target)
3390 } else {
3391 self.attributes.contains_key("relationship")
3392 }
3393 }
3394
3395 fn get_residency_months(&self) -> Option<u32> {
3396 self.attributes
3397 .get("residency_months")
3398 .and_then(|v| v.parse().ok())
3399 }
3400
3401 fn get_duration(&self, unit: DurationUnit) -> Option<u32> {
3402 let key = format!("duration_{:?}", unit).to_lowercase();
3403 self.attributes.get(&key).and_then(|v| v.parse().ok())
3404 }
3405
3406 fn get_percentage(&self, context: &str) -> Option<u32> {
3407 let key = format!("percentage_{}", context);
3408 self.attributes.get(&key).and_then(|v| v.parse().ok())
3409 }
3410
3411 fn evaluate_formula(&self, _formula: &str) -> Option<f64> {
3412 None
3414 }
3415}
3416
3417#[derive(Debug, Default)]
3455pub struct ConditionEvaluator {
3456 cache: std::collections::HashMap<String, bool>,
3457 cache_hits: usize,
3458 cache_misses: usize,
3459}
3460
3461impl ConditionEvaluator {
3462 #[must_use]
3464 pub fn new() -> Self {
3465 Self::default()
3466 }
3467
3468 pub fn evaluate<C: EvaluationContext>(
3472 &mut self,
3473 condition: &Condition,
3474 context: &C,
3475 ) -> Result<bool, EvaluationError> {
3476 let cache_key = format!("{}", condition);
3477
3478 if let Some(&result) = self.cache.get(&cache_key) {
3479 self.cache_hits += 1;
3480 return Ok(result);
3481 }
3482
3483 self.cache_misses += 1;
3484 let result = condition.evaluate(context)?;
3485 self.cache.insert(cache_key, result);
3486 Ok(result)
3487 }
3488
3489 pub fn clear_cache(&mut self) {
3491 self.cache.clear();
3492 self.cache_hits = 0;
3493 self.cache_misses = 0;
3494 }
3495
3496 #[must_use]
3498 pub const fn cache_hits(&self) -> usize {
3499 self.cache_hits
3500 }
3501
3502 #[must_use]
3504 pub const fn cache_misses(&self) -> usize {
3505 self.cache_misses
3506 }
3507
3508 #[must_use]
3510 pub fn hit_ratio(&self) -> f64 {
3511 let total = self.cache_hits + self.cache_misses;
3512 if total == 0 {
3513 0.0
3514 } else {
3515 self.cache_hits as f64 / total as f64
3516 }
3517 }
3518}
3519
3520#[cfg(feature = "parallel")]
3525use rayon::prelude::*;
3526
3527impl Condition {
3528 #[cfg(feature = "parallel")]
3564 pub fn evaluate_parallel<C: EvaluationContext + Sync>(
3565 &self,
3566 context: &C,
3567 ) -> Result<bool, EvaluationError> {
3568 self.evaluate_parallel_with_depth(context, 0, 100)
3569 }
3570
3571 #[cfg(feature = "parallel")]
3572 #[allow(clippy::too_many_lines)]
3573 fn evaluate_parallel_with_depth<C: EvaluationContext + Sync>(
3574 &self,
3575 context: &C,
3576 depth: usize,
3577 max_depth: usize,
3578 ) -> Result<bool, EvaluationError> {
3579 if depth > max_depth {
3580 return Err(EvaluationError::MaxDepthExceeded { max_depth });
3581 }
3582
3583 match self {
3584 Self::Age { .. }
3586 | Self::Income { .. }
3587 | Self::HasAttribute { .. }
3588 | Self::AttributeEquals { .. }
3589 | Self::DateRange { .. }
3590 | Self::Geographic { .. }
3591 | Self::EntityRelationship { .. }
3592 | Self::ResidencyDuration { .. }
3593 | Self::Duration { .. }
3594 | Self::Percentage { .. }
3595 | Self::SetMembership { .. }
3596 | Self::Pattern { .. }
3597 | Self::Calculation { .. }
3598 | Self::Threshold { .. }
3599 | Self::Fuzzy { .. }
3600 | Self::Temporal { .. }
3601 | Self::Custom { .. } => {
3602 self.evaluate(context)
3604 }
3605
3606 Self::Composite {
3608 conditions,
3609 threshold,
3610 } => {
3611 let results: Vec<_> = conditions
3613 .par_iter()
3614 .map(|(weight, cond)| {
3615 cond.evaluate_parallel_with_depth(context, depth + 1, max_depth)
3616 .map(|satisfied| if satisfied { *weight } else { 0.0 })
3617 })
3618 .collect();
3619
3620 for result in &results {
3622 if let Err(e) = result {
3623 return Err(e.clone());
3624 }
3625 }
3626
3627 let total_score: f64 = results.iter().filter_map(|r| r.as_ref().ok()).sum();
3629 Ok(total_score >= *threshold)
3630 }
3631
3632 Self::Probabilistic {
3633 condition,
3634 probability,
3635 threshold,
3636 } => {
3637 let satisfied =
3638 condition.evaluate_parallel_with_depth(context, depth + 1, max_depth)?;
3639 let effective_probability = if satisfied { *probability } else { 0.0 };
3640 Ok(effective_probability >= *threshold)
3641 }
3642
3643 Self::And(left, right) => {
3645 let (left_result, right_result) = rayon::join(
3647 || left.evaluate_parallel_with_depth(context, depth + 1, max_depth),
3648 || right.evaluate_parallel_with_depth(context, depth + 1, max_depth),
3649 );
3650
3651 match (left_result, right_result) {
3653 (Ok(true), Ok(true)) => Ok(true),
3654 (Ok(false), _) | (_, Ok(false)) => Ok(false),
3655 (Err(e), _) | (_, Err(e)) => Err(e),
3656 }
3657 }
3658
3659 Self::Or(left, right) => {
3660 let (left_result, right_result) = rayon::join(
3662 || left.evaluate_parallel_with_depth(context, depth + 1, max_depth),
3663 || right.evaluate_parallel_with_depth(context, depth + 1, max_depth),
3664 );
3665
3666 match (left_result, right_result) {
3668 (Ok(true), _) | (_, Ok(true)) => Ok(true),
3669 (Ok(false), Ok(false)) => Ok(false),
3670 (Err(e), _) | (_, Err(e)) => Err(e),
3671 }
3672 }
3673
3674 Self::Not(inner) => {
3675 let result = inner.evaluate_parallel_with_depth(context, depth + 1, max_depth)?;
3676 Ok(!result)
3677 }
3678 }
3679 }
3680
3681 #[cfg(feature = "parallel")]
3717 pub fn evaluate_all_parallel<C: EvaluationContext + Sync>(
3718 conditions: &[Condition],
3719 context: &C,
3720 ) -> Vec<Result<bool, EvaluationError>> {
3721 conditions
3722 .par_iter()
3723 .map(|cond| cond.evaluate_parallel(context))
3724 .collect()
3725 }
3726}
3727
3728#[cfg(feature = "parallel")]
3730impl ConditionEvaluator {
3731 pub fn evaluate_parallel<C: EvaluationContext + Sync>(
3736 &mut self,
3737 condition: &Condition,
3738 context: &C,
3739 ) -> Result<bool, EvaluationError> {
3740 let cache_key = format!("{}", condition);
3741
3742 if let Some(&result) = self.cache.get(&cache_key) {
3743 self.cache_hits += 1;
3744 return Ok(result);
3745 }
3746
3747 self.cache_misses += 1;
3748 let result = condition.evaluate_parallel(context)?;
3749 self.cache.insert(cache_key, result);
3750 Ok(result)
3751 }
3752}
3753
3754#[cfg(feature = "parallel")]
3756impl EntailmentEngine {
3757 pub fn entail_parallel<C: EvaluationContext + Sync>(
3762 &self,
3763 context: &C,
3764 ) -> Vec<EntailmentResult> {
3765 self.statutes
3766 .par_iter()
3767 .map(|statute| self.apply_statute_parallel(statute, context))
3768 .collect()
3769 }
3770
3771 pub fn entail_satisfied_parallel<C: EvaluationContext + Sync>(
3773 &self,
3774 context: &C,
3775 ) -> Vec<EntailmentResult> {
3776 self.entail_parallel(context)
3777 .into_par_iter()
3778 .filter(|result| result.conditions_satisfied)
3779 .collect()
3780 }
3781
3782 fn apply_statute_parallel<C: EvaluationContext + Sync>(
3783 &self,
3784 statute: &Statute,
3785 context: &C,
3786 ) -> EntailmentResult {
3787 let mut errors = Vec::new();
3788
3789 if statute.preconditions.is_empty() {
3790 return EntailmentResult {
3791 statute_id: statute.id.clone(),
3792 effect: statute.effect.clone(),
3793 conditions_satisfied: true,
3794 errors: Vec::new(),
3795 };
3796 }
3797
3798 let results: Vec<_> = statute
3800 .preconditions
3801 .par_iter()
3802 .map(|condition| condition.evaluate_parallel(context))
3803 .collect();
3804
3805 let all_satisfied = results.iter().all(|r| matches!(r, Ok(true)));
3806
3807 for result in results {
3808 if let Err(e) = result {
3809 errors.push(format!("{}", e));
3810 }
3811 }
3812
3813 EntailmentResult {
3814 statute_id: statute.id.clone(),
3815 effect: statute.effect.clone(),
3816 conditions_satisfied: all_satisfied,
3817 errors,
3818 }
3819 }
3820}
3821
3822pub struct SubsumptionAnalyzer;
3854
3855impl SubsumptionAnalyzer {
3856 #[must_use]
3860 pub fn subsumes(a: &Statute, b: &Statute) -> bool {
3861 if !Self::effects_compatible(&a.effect, &b.effect) {
3863 return false;
3864 }
3865
3866 if a.preconditions.is_empty() {
3868 return true;
3869 }
3870
3871 if b.preconditions.is_empty() {
3873 return false;
3874 }
3875
3876 Self::conditions_subsume(&a.preconditions, &b.preconditions)
3878 }
3879
3880 fn effects_compatible(a: &Effect, b: &Effect) -> bool {
3882 a.effect_type == b.effect_type && a.description == b.description
3884 }
3885
3886 fn conditions_subsume(a_conds: &[Condition], b_conds: &[Condition]) -> bool {
3890 for a_cond in a_conds {
3894 if !Self::condition_present_in(a_cond, b_conds) {
3895 return false;
3896 }
3897 }
3898
3899 true
3900 }
3901
3902 fn condition_present_in(a_cond: &Condition, b_conds: &[Condition]) -> bool {
3904 if b_conds
3906 .iter()
3907 .any(|b_cond| Self::conditions_equivalent(a_cond, b_cond))
3908 {
3909 return true;
3910 }
3911
3912 if b_conds
3914 .iter()
3915 .any(|b_cond| Self::condition_subsumes_condition(a_cond, b_cond))
3916 {
3917 return true;
3918 }
3919
3920 for b_cond in b_conds {
3922 if Self::condition_in_compound(a_cond, b_cond) {
3923 return true;
3924 }
3925 }
3926
3927 false
3928 }
3929
3930 fn condition_subsumes_condition(a: &Condition, b: &Condition) -> bool {
3932 match (a, b) {
3933 (
3935 Condition::Age {
3936 operator: op_a,
3937 value: val_a,
3938 },
3939 Condition::Age {
3940 operator: op_b,
3941 value: val_b,
3942 },
3943 ) => {
3944 match (op_a, op_b) {
3945 (ComparisonOp::GreaterOrEqual, ComparisonOp::GreaterOrEqual) => val_b >= val_a,
3947 (ComparisonOp::LessOrEqual, ComparisonOp::LessOrEqual) => val_b <= val_a,
3949 _ => false,
3950 }
3951 }
3952
3953 (
3955 Condition::Income {
3956 operator: op_a,
3957 value: val_a,
3958 },
3959 Condition::Income {
3960 operator: op_b,
3961 value: val_b,
3962 },
3963 ) => match (op_a, op_b) {
3964 (ComparisonOp::LessThan, ComparisonOp::LessThan) => val_b <= val_a,
3965 (ComparisonOp::GreaterThan, ComparisonOp::GreaterThan) => val_b >= val_a,
3966 _ => false,
3967 },
3968
3969 (
3971 Condition::Percentage {
3972 operator: op_a,
3973 value: val_a,
3974 context: ctx_a,
3975 },
3976 Condition::Percentage {
3977 operator: op_b,
3978 value: val_b,
3979 context: ctx_b,
3980 },
3981 ) => {
3982 if ctx_a != ctx_b {
3983 return false;
3984 }
3985 match (op_a, op_b) {
3986 (ComparisonOp::GreaterOrEqual, ComparisonOp::GreaterOrEqual) => val_b >= val_a,
3987 (ComparisonOp::LessOrEqual, ComparisonOp::LessOrEqual) => val_b <= val_a,
3988 _ => false,
3989 }
3990 }
3991
3992 (Condition::And(a_left, a_right), Condition::And(b_left, b_right)) => {
3994 Self::condition_subsumes_condition(a_left, b_left)
3995 && Self::condition_subsumes_condition(a_right, b_right)
3996 }
3997
3998 _ => false,
3999 }
4000 }
4001
4002 fn conditions_equivalent(a: &Condition, b: &Condition) -> bool {
4004 match (a, b) {
4005 (
4006 Condition::Age {
4007 operator: op_a,
4008 value: val_a,
4009 },
4010 Condition::Age {
4011 operator: op_b,
4012 value: val_b,
4013 },
4014 ) => op_a == op_b && val_a == val_b,
4015 (
4016 Condition::Income {
4017 operator: op_a,
4018 value: val_a,
4019 },
4020 Condition::Income {
4021 operator: op_b,
4022 value: val_b,
4023 },
4024 ) => op_a == op_b && val_a == val_b,
4025 (Condition::HasAttribute { key: key_a }, Condition::HasAttribute { key: key_b }) => {
4026 key_a == key_b
4027 }
4028 (
4029 Condition::AttributeEquals {
4030 key: key_a,
4031 value: val_a,
4032 },
4033 Condition::AttributeEquals {
4034 key: key_b,
4035 value: val_b,
4036 },
4037 ) => key_a == key_b && val_a == val_b,
4038 (
4039 Condition::Geographic {
4040 region_type: rt_a,
4041 region_id: rid_a,
4042 },
4043 Condition::Geographic {
4044 region_type: rt_b,
4045 region_id: rid_b,
4046 },
4047 ) => rt_a == rt_b && rid_a == rid_b,
4048 _ => false,
4049 }
4050 }
4051
4052 fn condition_in_compound(target: &Condition, compound: &Condition) -> bool {
4054 match compound {
4055 Condition::And(left, right) | Condition::Or(left, right) => {
4056 Self::conditions_equivalent(target, left)
4057 || Self::conditions_equivalent(target, right)
4058 || Self::condition_subsumes_condition(target, left)
4059 || Self::condition_subsumes_condition(target, right)
4060 || Self::condition_in_compound(target, left)
4061 || Self::condition_in_compound(target, right)
4062 }
4063 Condition::Not(inner) => {
4064 Self::conditions_equivalent(target, inner)
4065 || Self::condition_in_compound(target, inner)
4066 }
4067 _ => false,
4068 }
4069 }
4070
4071 #[must_use]
4075 pub fn find_subsumed<'a>(statute: &Statute, candidates: &'a [Statute]) -> Vec<&'a Statute> {
4076 candidates
4077 .iter()
4078 .filter(|candidate| candidate.id != statute.id && Self::subsumes(statute, candidate))
4079 .collect()
4080 }
4081
4082 #[must_use]
4086 pub fn find_subsuming<'a>(statute: &Statute, candidates: &'a [Statute]) -> Vec<&'a Statute> {
4087 candidates
4088 .iter()
4089 .filter(|candidate| candidate.id != statute.id && Self::subsumes(candidate, statute))
4090 .collect()
4091 }
4092}
4093
4094#[derive(Debug, Clone, PartialEq)]
4100#[cfg_attr(feature = "serde", derive(Serialize, Deserialize))]
4101#[cfg_attr(feature = "schema", derive(JsonSchema))]
4102pub struct EntailmentResult {
4103 pub statute_id: String,
4105 pub effect: Effect,
4107 pub conditions_satisfied: bool,
4109 pub errors: Vec<String>,
4111}
4112
4113#[derive(Debug, Clone)]
4147pub struct EntailmentEngine {
4148 statutes: Vec<Statute>,
4149}
4150
4151impl EntailmentEngine {
4152 #[must_use]
4154 pub fn new(statutes: Vec<Statute>) -> Self {
4155 Self { statutes }
4156 }
4157
4158 pub fn entail(&self, context: &AttributeBasedContext) -> Vec<EntailmentResult> {
4162 self.statutes
4163 .iter()
4164 .map(|statute| self.apply_statute(statute, context))
4165 .collect()
4166 }
4167
4168 pub fn entail_satisfied(&self, context: &AttributeBasedContext) -> Vec<EntailmentResult> {
4172 self.entail(context)
4173 .into_iter()
4174 .filter(|result| result.conditions_satisfied)
4175 .collect()
4176 }
4177
4178 fn apply_statute(
4180 &self,
4181 statute: &Statute,
4182 context: &AttributeBasedContext,
4183 ) -> EntailmentResult {
4184 let mut errors = Vec::new();
4185 let mut all_satisfied = true;
4186
4187 if statute.preconditions.is_empty() {
4189 return EntailmentResult {
4190 statute_id: statute.id.clone(),
4191 effect: statute.effect.clone(),
4192 conditions_satisfied: true,
4193 errors: Vec::new(),
4194 };
4195 }
4196
4197 for condition in &statute.preconditions {
4199 match condition.evaluate_simple(context) {
4200 Ok(true) => {
4201 }
4203 Ok(false) => {
4204 all_satisfied = false;
4205 }
4206 Err(e) => {
4207 all_satisfied = false;
4208 errors.push(format!("{}", e));
4209 }
4210 }
4211 }
4212
4213 EntailmentResult {
4214 statute_id: statute.id.clone(),
4215 effect: statute.effect.clone(),
4216 conditions_satisfied: all_satisfied,
4217 errors,
4218 }
4219 }
4220
4221 pub fn add_statute(&mut self, statute: Statute) {
4223 self.statutes.push(statute);
4224 }
4225
4226 pub fn remove_statute(&mut self, statute_id: &str) -> bool {
4228 let original_len = self.statutes.len();
4229 self.statutes.retain(|s| s.id != statute_id);
4230 self.statutes.len() < original_len
4231 }
4232
4233 #[must_use]
4235 pub fn statutes(&self) -> &[Statute] {
4236 &self.statutes
4237 }
4238
4239 #[must_use]
4241 pub fn statute_count(&self) -> usize {
4242 self.statutes.len()
4243 }
4244
4245 #[must_use]
4247 pub fn has_statute(&self, statute_id: &str) -> bool {
4248 self.statutes.iter().any(|s| s.id == statute_id)
4249 }
4250}
4251
4252#[derive(Debug, Clone, PartialEq)]
4254#[cfg_attr(feature = "serde", derive(Serialize, Deserialize))]
4255#[cfg_attr(feature = "schema", derive(JsonSchema))]
4256pub struct InferenceStep {
4257 pub statute_id: String,
4259 pub effect: Effect,
4261 pub depends_on: Vec<usize>,
4263}
4264
4265#[derive(Debug, Clone)]
4297pub struct ForwardChainingEngine {
4298 statutes: Vec<Statute>,
4299}
4300
4301impl ForwardChainingEngine {
4302 #[must_use]
4304 pub fn new(statutes: Vec<Statute>) -> Self {
4305 Self { statutes }
4306 }
4307
4308 pub fn infer(&self, context: &AttributeBasedContext, max_steps: usize) -> Vec<InferenceStep> {
4312 let mut inferences = Vec::new();
4313 let mut changed = true;
4314 let mut steps = 0;
4315
4316 while changed && steps < max_steps {
4317 changed = false;
4318 steps += 1;
4319
4320 for statute in &self.statutes {
4321 if inferences
4323 .iter()
4324 .any(|inf: &InferenceStep| inf.statute_id == statute.id)
4325 {
4326 continue;
4327 }
4328
4329 if self.can_apply_statute(statute, context) {
4331 let depends_on = self.find_dependencies(&inferences, statute);
4332
4333 inferences.push(InferenceStep {
4334 statute_id: statute.id.clone(),
4335 effect: statute.effect.clone(),
4336 depends_on,
4337 });
4338
4339 changed = true;
4340 }
4341 }
4342 }
4343
4344 inferences
4345 }
4346
4347 fn can_apply_statute(&self, statute: &Statute, context: &AttributeBasedContext) -> bool {
4349 if statute.preconditions.is_empty() {
4350 return true;
4351 }
4352
4353 statute
4354 .preconditions
4355 .iter()
4356 .all(|cond| cond.evaluate_simple(context).unwrap_or(false))
4357 }
4358
4359 fn find_dependencies(&self, _inferences: &[InferenceStep], _statute: &Statute) -> Vec<usize> {
4361 Vec::new()
4364 }
4365}
4366
4367impl fmt::Display for EntailmentResult {
4368 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
4369 write!(
4370 f,
4371 "{}: {} (satisfied: {})",
4372 self.statute_id, self.effect, self.conditions_satisfied
4373 )
4374 }
4375}
4376
4377#[derive(Debug, Clone, PartialEq)]
4383#[cfg_attr(feature = "serde", derive(Serialize, Deserialize))]
4384#[cfg_attr(feature = "schema", derive(JsonSchema))]
4385pub struct LegalExplanation {
4386 pub outcome: Effect,
4388 pub applicable_statutes: Vec<String>,
4390 pub satisfied_conditions: Vec<String>,
4392 pub unsatisfied_conditions: Vec<String>,
4394 pub confidence: f64,
4396 pub reasoning_chain: Vec<ReasoningStep>,
4398}
4399
4400#[derive(Debug, Clone, PartialEq)]
4402#[cfg_attr(feature = "serde", derive(Serialize, Deserialize))]
4403#[cfg_attr(feature = "schema", derive(JsonSchema))]
4404pub struct ReasoningStep {
4405 pub step: usize,
4407 pub description: String,
4409 pub statute_id: Option<String>,
4411 pub condition: Option<String>,
4413 pub result: StepResult,
4415}
4416
4417#[derive(Debug, Clone, Copy, PartialEq, Eq)]
4419#[cfg_attr(feature = "serde", derive(Serialize, Deserialize))]
4420#[cfg_attr(feature = "schema", derive(JsonSchema))]
4421pub enum StepResult {
4422 Satisfied,
4424 NotSatisfied,
4426 Applied,
4428 NotApplicable,
4430 Uncertain,
4432}
4433
4434impl fmt::Display for StepResult {
4435 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
4436 match self {
4437 Self::Satisfied => write!(f, "✓ satisfied"),
4438 Self::NotSatisfied => write!(f, "✗ not satisfied"),
4439 Self::Applied => write!(f, "→ applied"),
4440 Self::NotApplicable => write!(f, "- not applicable"),
4441 Self::Uncertain => write!(f, "? uncertain"),
4442 }
4443 }
4444}
4445
4446impl fmt::Display for LegalExplanation {
4447 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
4448 writeln!(f, "Explanation for outcome: {}", self.outcome)?;
4449 writeln!(f, "Confidence: {:.0}%", self.confidence * 100.0)?;
4450 writeln!(f)?;
4451
4452 if !self.applicable_statutes.is_empty() {
4453 writeln!(f, "Applicable statutes:")?;
4454 for statute_id in &self.applicable_statutes {
4455 writeln!(f, " - {}", statute_id)?;
4456 }
4457 writeln!(f)?;
4458 }
4459
4460 if !self.satisfied_conditions.is_empty() {
4461 writeln!(f, "Satisfied conditions:")?;
4462 for condition in &self.satisfied_conditions {
4463 writeln!(f, " ✓ {}", condition)?;
4464 }
4465 writeln!(f)?;
4466 }
4467
4468 if !self.unsatisfied_conditions.is_empty() {
4469 writeln!(f, "Unsatisfied conditions:")?;
4470 for condition in &self.unsatisfied_conditions {
4471 writeln!(f, " ✗ {}", condition)?;
4472 }
4473 writeln!(f)?;
4474 }
4475
4476 if !self.reasoning_chain.is_empty() {
4477 writeln!(f, "Reasoning chain:")?;
4478 for step in &self.reasoning_chain {
4479 write!(f, " {}. {} ", step.step, step.description)?;
4480 writeln!(f, "[{}]", step.result)?;
4481 }
4482 }
4483
4484 Ok(())
4485 }
4486}
4487
4488#[derive(Debug, Clone)]
4528pub struct AbductiveReasoner {
4529 statutes: Vec<Statute>,
4530}
4531
4532impl AbductiveReasoner {
4533 #[must_use]
4535 pub fn new(statutes: Vec<Statute>) -> Self {
4536 Self { statutes }
4537 }
4538
4539 pub fn explain_effect<C: EvaluationContext>(
4543 &self,
4544 target_effect: Effect,
4545 context: &C,
4546 ) -> Vec<LegalExplanation> {
4547 let mut explanations = Vec::new();
4548
4549 for statute in &self.statutes {
4551 if self.effects_match(&statute.effect, &target_effect)
4552 && let Some(explanation) = self.explain_statute(statute, context)
4553 {
4554 explanations.push(explanation);
4555 }
4556 }
4557
4558 explanations.sort_by(|a, b| b.confidence.partial_cmp(&a.confidence).unwrap());
4560
4561 explanations
4562 }
4563
4564 pub fn explain_statute<C: EvaluationContext>(
4566 &self,
4567 statute: &Statute,
4568 context: &C,
4569 ) -> Option<LegalExplanation> {
4570 let mut reasoning_chain = Vec::new();
4571 let mut satisfied_conditions = Vec::new();
4572 let mut unsatisfied_conditions = Vec::new();
4573 let mut step_num = 1;
4574
4575 if statute.preconditions.is_empty() {
4577 reasoning_chain.push(ReasoningStep {
4578 step: step_num,
4579 description: format!("Statute '{}' has no preconditions", statute.id),
4580 statute_id: Some(statute.id.clone()),
4581 condition: None,
4582 result: StepResult::Applied,
4583 });
4584
4585 return Some(LegalExplanation {
4586 outcome: statute.effect.clone(),
4587 applicable_statutes: vec![statute.id.clone()],
4588 satisfied_conditions: vec!["No preconditions".to_string()],
4589 unsatisfied_conditions: Vec::new(),
4590 confidence: 1.0,
4591 reasoning_chain,
4592 });
4593 }
4594
4595 for condition in &statute.preconditions {
4597 let condition_str = format!("{}", condition);
4598
4599 match condition.evaluate(context) {
4600 Ok(true) => {
4601 satisfied_conditions.push(condition_str.clone());
4602 reasoning_chain.push(ReasoningStep {
4603 step: step_num,
4604 description: format!("Condition satisfied: {}", condition_str),
4605 statute_id: Some(statute.id.clone()),
4606 condition: Some(condition_str),
4607 result: StepResult::Satisfied,
4608 });
4609 }
4610 Ok(false) => {
4611 unsatisfied_conditions.push(condition_str.clone());
4612 reasoning_chain.push(ReasoningStep {
4613 step: step_num,
4614 description: format!("Condition not satisfied: {}", condition_str),
4615 statute_id: Some(statute.id.clone()),
4616 condition: Some(condition_str),
4617 result: StepResult::NotSatisfied,
4618 });
4619 }
4620 Err(_) => {
4621 unsatisfied_conditions.push(condition_str.clone());
4622 reasoning_chain.push(ReasoningStep {
4623 step: step_num,
4624 description: format!("Condition evaluation failed: {}", condition_str),
4625 statute_id: Some(statute.id.clone()),
4626 condition: Some(condition_str),
4627 result: StepResult::Uncertain,
4628 });
4629 }
4630 }
4631
4632 step_num += 1;
4633 }
4634
4635 let total_conditions = statute.preconditions.len();
4637 let satisfied_count = satisfied_conditions.len();
4638 let confidence = if total_conditions > 0 {
4639 satisfied_count as f64 / total_conditions as f64
4640 } else {
4641 1.0
4642 };
4643
4644 let all_satisfied = unsatisfied_conditions.is_empty();
4646 let applicable_statutes = if all_satisfied {
4647 vec![statute.id.clone()]
4648 } else {
4649 Vec::new()
4650 };
4651
4652 reasoning_chain.push(ReasoningStep {
4653 step: step_num,
4654 description: if all_satisfied {
4655 format!("Statute '{}' applies", statute.id)
4656 } else {
4657 format!("Statute '{}' does not apply", statute.id)
4658 },
4659 statute_id: Some(statute.id.clone()),
4660 condition: None,
4661 result: if all_satisfied {
4662 StepResult::Applied
4663 } else {
4664 StepResult::NotApplicable
4665 },
4666 });
4667
4668 Some(LegalExplanation {
4669 outcome: statute.effect.clone(),
4670 applicable_statutes,
4671 satisfied_conditions,
4672 unsatisfied_conditions,
4673 confidence,
4674 reasoning_chain,
4675 })
4676 }
4677
4678 pub fn explain_why_not<C: EvaluationContext>(
4683 &self,
4684 target_effect: Effect,
4685 context: &C,
4686 ) -> Vec<LegalExplanation> {
4687 let mut explanations = Vec::new();
4688
4689 for statute in &self.statutes {
4691 if self.effects_match(&statute.effect, &target_effect)
4692 && let Some(explanation) = self.explain_statute(statute, context)
4693 {
4694 if explanation.confidence < 1.0 {
4696 explanations.push(explanation);
4697 }
4698 }
4699 }
4700
4701 explanations.sort_by(|a, b| b.confidence.partial_cmp(&a.confidence).unwrap());
4703
4704 explanations
4705 }
4706
4707 fn effects_match(&self, effect1: &Effect, effect2: &Effect) -> bool {
4709 effect1.effect_type == effect2.effect_type
4710 && effect1.description.contains(&effect2.description)
4711 }
4712
4713 pub fn find_alternatives<C: EvaluationContext>(
4718 &self,
4719 target_effect: Effect,
4720 context: &C,
4721 ) -> Vec<LegalExplanation> {
4722 let mut alternatives = Vec::new();
4723
4724 for statute in &self.statutes {
4725 if self.effects_match(&statute.effect, &target_effect)
4726 && let Some(explanation) = self.explain_statute(statute, context)
4727 {
4728 alternatives.push(explanation);
4729 }
4730 }
4731
4732 alternatives
4733 }
4734}
4735
4736pub struct StatuteQuery<'a> {
4769 statutes: &'a [Statute],
4770 #[allow(clippy::type_complexity)]
4771 filters: Vec<Box<dyn Fn(&Statute) -> bool + 'a>>,
4772}
4773
4774impl<'a> StatuteQuery<'a> {
4775 #[must_use]
4777 pub fn new(statutes: &'a [Statute]) -> Self {
4778 Self {
4779 statutes,
4780 filters: Vec::new(),
4781 }
4782 }
4783
4784 #[must_use]
4786 pub fn jurisdiction(mut self, jurisdiction: &'a str) -> Self {
4787 self.filters.push(Box::new(move |s| {
4788 s.jurisdiction.as_ref().is_some_and(|j| j == jurisdiction)
4789 }));
4790 self
4791 }
4792
4793 #[must_use]
4795 pub fn jurisdiction_prefix(mut self, prefix: &'a str) -> Self {
4796 self.filters.push(Box::new(move |s| {
4797 s.jurisdiction
4798 .as_ref()
4799 .is_some_and(|j| j.starts_with(prefix))
4800 }));
4801 self
4802 }
4803
4804 #[must_use]
4806 pub fn effect_type(mut self, effect_type: EffectType) -> Self {
4807 self.filters
4808 .push(Box::new(move |s| s.effect.effect_type == effect_type));
4809 self
4810 }
4811
4812 #[must_use]
4814 pub fn grants(mut self, description: &'a str) -> Self {
4815 self.filters.push(Box::new(move |s| {
4816 s.effect.effect_type == EffectType::Grant && s.effect.description.contains(description)
4817 }));
4818 self
4819 }
4820
4821 #[must_use]
4823 pub fn revokes(mut self, description: &'a str) -> Self {
4824 self.filters.push(Box::new(move |s| {
4825 s.effect.effect_type == EffectType::Revoke && s.effect.description.contains(description)
4826 }));
4827 self
4828 }
4829
4830 #[must_use]
4832 pub fn with_preconditions(mut self) -> Self {
4833 self.filters.push(Box::new(|s| !s.preconditions.is_empty()));
4834 self
4835 }
4836
4837 #[must_use]
4839 pub fn unconditional(mut self) -> Self {
4840 self.filters.push(Box::new(|s| s.preconditions.is_empty()));
4841 self
4842 }
4843
4844 #[must_use]
4846 pub fn min_preconditions(mut self, min: usize) -> Self {
4847 self.filters
4848 .push(Box::new(move |s| s.preconditions.len() >= min));
4849 self
4850 }
4851
4852 #[must_use]
4854 pub fn effective_at(mut self, date: NaiveDate) -> Self {
4855 self.filters
4856 .push(Box::new(move |s| s.temporal_validity.is_active(date)));
4857 self
4858 }
4859
4860 #[must_use]
4862 pub fn currently_effective(mut self) -> Self {
4863 let today = chrono::Utc::now().date_naive();
4864 self.filters
4865 .push(Box::new(move |s| s.temporal_validity.is_active(today)));
4866 self
4867 }
4868
4869 #[must_use]
4871 pub fn version(mut self, version: u32) -> Self {
4872 self.filters.push(Box::new(move |s| s.version == version));
4873 self
4874 }
4875
4876 #[must_use]
4878 pub fn id_prefix(mut self, prefix: &'a str) -> Self {
4879 self.filters
4880 .push(Box::new(move |s| s.id.starts_with(prefix)));
4881 self
4882 }
4883
4884 #[must_use]
4886 pub fn id_suffix(mut self, suffix: &'a str) -> Self {
4887 self.filters.push(Box::new(move |s| s.id.ends_with(suffix)));
4888 self
4889 }
4890
4891 #[must_use]
4893 pub fn keyword(mut self, keyword: &'a str) -> Self {
4894 self.filters.push(Box::new(move |s| {
4895 s.id.contains(keyword) || s.title.contains(keyword)
4896 }));
4897 self
4898 }
4899
4900 #[must_use]
4902 pub fn filter<F>(mut self, predicate: F) -> Self
4903 where
4904 F: Fn(&Statute) -> bool + 'a,
4905 {
4906 self.filters.push(Box::new(predicate));
4907 self
4908 }
4909
4910 #[must_use]
4912 pub fn execute(self) -> Vec<&'a Statute> {
4913 self.statutes
4914 .iter()
4915 .filter(|statute| self.filters.iter().all(|f| f(statute)))
4916 .collect()
4917 }
4918
4919 #[must_use]
4921 pub fn first(self) -> Option<&'a Statute> {
4922 self.statutes
4923 .iter()
4924 .find(|statute| self.filters.iter().all(|f| f(statute)))
4925 }
4926
4927 #[must_use]
4929 pub fn count(self) -> usize {
4930 self.statutes
4931 .iter()
4932 .filter(|statute| self.filters.iter().all(|f| f(statute)))
4933 .count()
4934 }
4935
4936 #[must_use]
4938 pub fn exists(self) -> bool {
4939 self.statutes
4940 .iter()
4941 .any(|statute| self.filters.iter().all(|f| f(statute)))
4942 }
4943}
4944
4945#[derive(Debug, Clone, Default)]
4963pub struct StatuteRegistry {
4964 statutes: Vec<Statute>,
4965}
4966
4967impl StatuteRegistry {
4968 #[must_use]
4970 pub fn new() -> Self {
4971 Self::default()
4972 }
4973
4974 #[must_use]
4976 pub fn from_statutes(statutes: Vec<Statute>) -> Self {
4977 Self { statutes }
4978 }
4979
4980 pub fn add(&mut self, statute: Statute) {
4982 self.statutes.push(statute);
4983 }
4984
4985 pub fn remove(&mut self, id: &str) -> bool {
4989 let original_len = self.statutes.len();
4990 self.statutes.retain(|s| s.id != id);
4991 self.statutes.len() < original_len
4992 }
4993
4994 #[must_use]
4996 pub fn get(&self, id: &str) -> Option<&Statute> {
4997 self.statutes.iter().find(|s| s.id == id)
4998 }
4999
5000 pub fn get_mut(&mut self, id: &str) -> Option<&mut Statute> {
5002 self.statutes.iter_mut().find(|s| s.id == id)
5003 }
5004
5005 #[must_use]
5007 pub fn len(&self) -> usize {
5008 self.statutes.len()
5009 }
5010
5011 #[must_use]
5013 pub fn is_empty(&self) -> bool {
5014 self.statutes.is_empty()
5015 }
5016
5017 pub fn iter(&self) -> impl Iterator<Item = &Statute> {
5019 self.statutes.iter()
5020 }
5021
5022 pub fn iter_mut(&mut self) -> impl Iterator<Item = &mut Statute> {
5024 self.statutes.iter_mut()
5025 }
5026
5027 #[must_use]
5029 pub fn query(&self) -> StatuteQuery<'_> {
5030 StatuteQuery::new(&self.statutes)
5031 }
5032
5033 pub fn clear(&mut self) {
5035 self.statutes.clear();
5036 }
5037
5038 #[must_use]
5040 pub fn all(&self) -> &[Statute] {
5041 &self.statutes
5042 }
5043
5044 #[must_use]
5046 pub fn find_conflicts(&self, date: NaiveDate) -> Vec<(&Statute, &Statute)> {
5047 let mut conflicts = Vec::new();
5048 let effective: Vec<_> = self
5049 .statutes
5050 .iter()
5051 .filter(|s| s.temporal_validity.is_active(date))
5052 .collect();
5053
5054 for i in 0..effective.len() {
5055 for j in (i + 1)..effective.len() {
5056 let a = effective[i];
5057 let b = effective[j];
5058
5059 if a.effect.effect_type == b.effect.effect_type
5061 && a.effect.description != b.effect.description
5062 && !a.preconditions.is_empty()
5063 && !b.preconditions.is_empty()
5064 {
5065 conflicts.push((a, b));
5066 }
5067 }
5068 }
5069
5070 conflicts
5071 }
5072
5073 pub fn merge(&mut self, other: StatuteRegistry) {
5075 self.statutes.extend(other.statutes);
5076 }
5077}
5078
5079impl IntoIterator for StatuteRegistry {
5080 type Item = Statute;
5081 type IntoIter = std::vec::IntoIter<Statute>;
5082
5083 fn into_iter(self) -> Self::IntoIter {
5084 self.statutes.into_iter()
5085 }
5086}
5087
5088impl<'a> IntoIterator for &'a StatuteRegistry {
5089 type Item = &'a Statute;
5090 type IntoIter = std::slice::Iter<'a, Statute>;
5091
5092 fn into_iter(self) -> Self::IntoIter {
5093 self.statutes.iter()
5094 }
5095}
5096
5097impl FromIterator<Statute> for StatuteRegistry {
5098 fn from_iter<T: IntoIterator<Item = Statute>>(iter: T) -> Self {
5099 Self {
5100 statutes: iter.into_iter().collect(),
5101 }
5102 }
5103}
5104
5105#[derive(Debug, Clone)]
5128#[cfg_attr(feature = "serde", derive(Serialize, Deserialize))]
5129#[cfg_attr(feature = "schema", derive(JsonSchema))]
5130pub struct StatuteGraph {
5131 statutes: std::collections::HashMap<String, Statute>,
5133 derivation_edges: std::collections::HashMap<String, Vec<String>>,
5135}
5136
5137impl StatuteGraph {
5138 #[must_use]
5140 pub fn new() -> Self {
5141 Self {
5142 statutes: std::collections::HashMap::new(),
5143 derivation_edges: std::collections::HashMap::new(),
5144 }
5145 }
5146
5147 pub fn add_statute(&mut self, statute: Statute) {
5163 let id = statute.id.clone();
5164
5165 for source_id in &statute.derives_from {
5167 self.derivation_edges
5168 .entry(source_id.clone())
5169 .or_default()
5170 .push(id.clone());
5171 }
5172
5173 self.statutes.insert(id, statute);
5174 }
5175
5176 #[must_use]
5178 pub fn len(&self) -> usize {
5179 self.statutes.len()
5180 }
5181
5182 #[must_use]
5184 pub fn is_empty(&self) -> bool {
5185 self.statutes.is_empty()
5186 }
5187
5188 #[must_use]
5190 pub fn get(&self, id: &str) -> Option<&Statute> {
5191 self.statutes.get(id)
5192 }
5193
5194 #[must_use]
5212 pub fn find_derived_from(&self, source_id: &str) -> Vec<&Statute> {
5213 self.derivation_edges
5214 .get(source_id)
5215 .map(|ids| ids.iter().filter_map(|id| self.statutes.get(id)).collect())
5216 .unwrap_or_default()
5217 }
5218
5219 #[must_use]
5238 pub fn find_sources(&self, statute_id: &str) -> Vec<&Statute> {
5239 self.statutes
5240 .get(statute_id)
5241 .map(|statute| {
5242 statute
5243 .derives_from
5244 .iter()
5245 .filter_map(|id| self.statutes.get(id))
5246 .collect()
5247 })
5248 .unwrap_or_default()
5249 }
5250
5251 #[must_use]
5272 pub fn find_all_derived_from(&self, source_id: &str) -> Vec<&Statute> {
5273 let mut result = Vec::new();
5274 let mut visited = std::collections::HashSet::new();
5275 let mut queue = vec![source_id];
5276
5277 while let Some(current_id) = queue.pop() {
5278 if !visited.insert(current_id) {
5279 continue; }
5281
5282 if let Some(derived_ids) = self.derivation_edges.get(current_id) {
5283 for derived_id in derived_ids {
5284 if let Some(statute) = self.statutes.get(derived_id) {
5285 result.push(statute);
5286 queue.push(derived_id);
5287 }
5288 }
5289 }
5290 }
5291
5292 result
5293 }
5294
5295 #[must_use]
5314 pub fn detect_cycles(&self) -> Vec<Vec<String>> {
5315 let mut cycles = Vec::new();
5316 let mut visited = std::collections::HashSet::new();
5317 let mut rec_stack = std::collections::HashSet::new();
5318 let mut path = Vec::new();
5319
5320 for id in self.statutes.keys() {
5321 if !visited.contains(id.as_str()) {
5322 self.detect_cycles_dfs(id, &mut visited, &mut rec_stack, &mut path, &mut cycles);
5323 }
5324 }
5325
5326 cycles
5327 }
5328
5329 #[allow(clippy::too_many_arguments)]
5330 fn detect_cycles_dfs(
5331 &self,
5332 node: &str,
5333 visited: &mut std::collections::HashSet<String>,
5334 rec_stack: &mut std::collections::HashSet<String>,
5335 path: &mut Vec<String>,
5336 cycles: &mut Vec<Vec<String>>,
5337 ) {
5338 visited.insert(node.to_string());
5339 rec_stack.insert(node.to_string());
5340 path.push(node.to_string());
5341
5342 if let Some(neighbors) = self.derivation_edges.get(node) {
5343 for neighbor in neighbors {
5344 if !visited.contains(neighbor) {
5345 self.detect_cycles_dfs(neighbor, visited, rec_stack, path, cycles);
5346 } else if rec_stack.contains(neighbor) {
5347 if let Some(pos) = path.iter().position(|x| x == neighbor) {
5349 cycles.push(path[pos..].to_vec());
5350 }
5351 }
5352 }
5353 }
5354
5355 path.pop();
5356 rec_stack.remove(node);
5357 }
5358
5359 pub fn iter(&self) -> impl Iterator<Item = &Statute> {
5361 self.statutes.values()
5362 }
5363}
5364
5365impl Default for StatuteGraph {
5366 fn default() -> Self {
5367 Self::new()
5368 }
5369}
5370
5371pub struct CrossJurisdictionAnalyzer {
5396 similarity_threshold: f64,
5398}
5399
5400impl CrossJurisdictionAnalyzer {
5401 #[must_use]
5403 pub fn new() -> Self {
5404 Self {
5405 similarity_threshold: 0.7,
5406 }
5407 }
5408
5409 #[must_use]
5419 pub fn with_threshold(threshold: f64) -> Self {
5420 Self {
5421 similarity_threshold: threshold.clamp(0.0, 1.0),
5422 }
5423 }
5424
5425 #[must_use]
5447 pub fn find_equivalents<'a>(
5448 &self,
5449 reference: &Statute,
5450 candidates: &'a [Statute],
5451 ) -> Vec<&'a Statute> {
5452 candidates
5453 .iter()
5454 .filter(|candidate| {
5455 if reference.jurisdiction == candidate.jurisdiction {
5457 return false;
5458 }
5459
5460 let similarity = self.calculate_similarity(reference, candidate);
5461 similarity >= self.similarity_threshold
5462 })
5463 .collect()
5464 }
5465
5466 #[must_use]
5470 pub fn calculate_similarity(&self, s1: &Statute, s2: &Statute) -> f64 {
5471 let mut score = 0.0;
5472 let mut weight_sum = 0.0;
5473
5474 let effect_weight = 0.4;
5476 if s1.effect.effect_type == s2.effect.effect_type {
5477 score += effect_weight;
5478 }
5479 weight_sum += effect_weight;
5480
5481 let precond_weight = 0.3;
5483 let precond_similarity = if s1.preconditions.is_empty() && s2.preconditions.is_empty() {
5484 1.0
5485 } else {
5486 let min_count = s1.preconditions.len().min(s2.preconditions.len()) as f64;
5487 let max_count = s1.preconditions.len().max(s2.preconditions.len()) as f64;
5488 if max_count == 0.0 {
5489 0.0
5490 } else {
5491 min_count / max_count
5492 }
5493 };
5494 score += precond_weight * precond_similarity;
5495 weight_sum += precond_weight;
5496
5497 let entity_weight = 0.3;
5499 let entity_similarity = if s1.applies_to.is_empty() && s2.applies_to.is_empty() {
5500 1.0 } else {
5502 let common = s1
5503 .applies_to
5504 .iter()
5505 .filter(|t| s2.applies_to.contains(t))
5506 .count() as f64;
5507 let total = (s1.applies_to.len() + s2.applies_to.len()) as f64;
5508 if total == 0.0 {
5509 0.0
5510 } else {
5511 2.0 * common / total }
5513 };
5514 score += entity_weight * entity_similarity;
5515 weight_sum += entity_weight;
5516
5517 score / weight_sum
5518 }
5519}
5520
5521impl Default for CrossJurisdictionAnalyzer {
5522 fn default() -> Self {
5523 Self::new()
5524 }
5525}
5526
5527#[derive(Debug, Clone, PartialEq)]
5555#[cfg_attr(feature = "serde", derive(Serialize, Deserialize))]
5556#[cfg_attr(feature = "schema", derive(JsonSchema))]
5557pub struct Effect {
5558 pub effect_type: EffectType,
5560 pub description: String,
5562 pub parameters: std::collections::HashMap<String, String>,
5564}
5565
5566impl Effect {
5567 pub fn new(effect_type: EffectType, description: impl Into<String>) -> Self {
5569 Self {
5570 effect_type,
5571 description: description.into(),
5572 parameters: std::collections::HashMap::new(),
5573 }
5574 }
5575
5576 pub fn with_parameter(mut self, key: impl Into<String>, value: impl Into<String>) -> Self {
5578 self.parameters.insert(key.into(), value.into());
5579 self
5580 }
5581
5582 #[must_use]
5584 pub fn get_parameter(&self, key: &str) -> Option<&String> {
5585 self.parameters.get(key)
5586 }
5587
5588 #[must_use]
5590 pub fn has_parameter(&self, key: &str) -> bool {
5591 self.parameters.contains_key(key)
5592 }
5593
5594 #[must_use]
5596 pub fn parameter_count(&self) -> usize {
5597 self.parameters.len()
5598 }
5599
5600 pub fn remove_parameter(&mut self, key: &str) -> Option<String> {
5602 self.parameters.remove(key)
5603 }
5604
5605 pub fn grant(description: impl Into<String>) -> Self {
5607 Self::new(EffectType::Grant, description)
5608 }
5609
5610 pub fn revoke(description: impl Into<String>) -> Self {
5612 Self::new(EffectType::Revoke, description)
5613 }
5614
5615 pub fn obligation(description: impl Into<String>) -> Self {
5617 Self::new(EffectType::Obligation, description)
5618 }
5619
5620 pub fn prohibition(description: impl Into<String>) -> Self {
5622 Self::new(EffectType::Prohibition, description)
5623 }
5624
5625 pub fn compose(effects: Vec<Effect>) -> ComposedEffect {
5643 ComposedEffect::new(effects)
5644 }
5645
5646 #[must_use]
5669 pub fn inverse(&self) -> Option<Effect> {
5670 let (inverse_type, inverse_description) = match self.effect_type {
5671 EffectType::Grant => (EffectType::Revoke, self.description.clone()),
5672 EffectType::Revoke => (EffectType::Grant, self.description.clone()),
5673 EffectType::Obligation => (
5674 EffectType::Grant,
5675 format!("relief from {}", self.description),
5676 ),
5677 EffectType::Prohibition => (
5678 EffectType::Grant,
5679 format!("permission for {}", self.description),
5680 ),
5681 EffectType::MonetaryTransfer => {
5682 let desc = if self.description.contains("tax") {
5684 self.description.replace("tax", "refund")
5685 } else if self.description.contains("fine") {
5686 self.description.replace("fine", "reimbursement")
5687 } else {
5688 format!("reverse {}", self.description)
5689 };
5690 (EffectType::MonetaryTransfer, desc)
5691 }
5692 EffectType::StatusChange => {
5693 (
5696 EffectType::StatusChange,
5697 format!("reverse {}", self.description),
5698 )
5699 }
5700 EffectType::Custom => return None, };
5702
5703 let mut inverse = Effect::new(inverse_type, inverse_description);
5704 inverse.parameters = self.parameters.clone();
5706 inverse
5707 .parameters
5708 .insert("_is_inverse".to_string(), "true".to_string());
5709 inverse.parameters.insert(
5710 "_original_type".to_string(),
5711 format!("{:?}", self.effect_type),
5712 );
5713
5714 Some(inverse)
5715 }
5716
5717 #[must_use]
5719 pub fn is_inverse_of(&self, other: &Effect) -> bool {
5720 if let Some(inv) = other.inverse() {
5721 self.effect_type == inv.effect_type
5722 && (self.description == inv.description
5723 || self.description.contains(&other.description))
5724 } else {
5725 false
5726 }
5727 }
5728
5729 #[must_use]
5746 pub fn with_temporal_validity(
5747 self,
5748 start: NaiveDate,
5749 end: Option<NaiveDate>,
5750 recurrence: Option<RecurrencePattern>,
5751 ) -> TemporalEffect {
5752 TemporalEffect::new(self, start, end, recurrence)
5753 }
5754
5755 #[must_use]
5770 pub fn when(self, condition: Condition) -> ConditionalEffect {
5771 ConditionalEffect::new(self, condition)
5772 }
5773}
5774
5775impl fmt::Display for Effect {
5776 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
5777 write!(f, "{}: {}", self.effect_type, self.description)
5778 }
5779}
5780
5781#[derive(Debug, Clone, PartialEq)]
5799#[cfg_attr(feature = "serde", derive(Serialize, Deserialize))]
5800#[cfg_attr(feature = "schema", derive(JsonSchema))]
5801pub struct ComposedEffect {
5802 pub effects: Vec<Effect>,
5804 pub resolution_strategy: CompositionStrategy,
5806}
5807
5808impl ComposedEffect {
5809 #[must_use]
5811 pub fn new(effects: Vec<Effect>) -> Self {
5812 Self {
5813 effects,
5814 resolution_strategy: CompositionStrategy::FirstWins,
5815 }
5816 }
5817
5818 #[must_use]
5820 pub fn with_resolution_strategy(mut self, strategy: CompositionStrategy) -> Self {
5821 self.resolution_strategy = strategy;
5822 self
5823 }
5824
5825 pub fn add_effect(&mut self, effect: Effect) {
5827 self.effects.push(effect);
5828 }
5829
5830 #[must_use]
5832 pub fn len(&self) -> usize {
5833 self.effects.len()
5834 }
5835
5836 #[must_use]
5838 pub fn is_empty(&self) -> bool {
5839 self.effects.is_empty()
5840 }
5841
5842 #[must_use]
5846 pub fn resolve(&self) -> Vec<&Effect> {
5847 match self.resolution_strategy {
5848 CompositionStrategy::FirstWins => {
5849 let mut seen_types = std::collections::HashSet::new();
5851 self.effects
5852 .iter()
5853 .filter(|e| seen_types.insert(e.effect_type.clone()))
5854 .collect()
5855 }
5856 CompositionStrategy::LastWins => {
5857 let mut result = std::collections::HashMap::new();
5859 for effect in &self.effects {
5860 result.insert(effect.effect_type.clone(), effect);
5861 }
5862 result.values().copied().collect()
5863 }
5864 CompositionStrategy::MostSpecific => {
5865 let mut result = std::collections::HashMap::new();
5867 for effect in &self.effects {
5868 result
5869 .entry(effect.effect_type.clone())
5870 .and_modify(|e: &mut &Effect| {
5871 if effect.parameter_count() > e.parameter_count() {
5872 *e = effect;
5873 }
5874 })
5875 .or_insert(effect);
5876 }
5877 result.values().copied().collect()
5878 }
5879 CompositionStrategy::AllApply => {
5880 self.effects.iter().collect()
5882 }
5883 }
5884 }
5885}
5886
5887impl fmt::Display for ComposedEffect {
5888 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
5889 write!(f, "ComposedEffect[")?;
5890 for (i, effect) in self.effects.iter().enumerate() {
5891 if i > 0 {
5892 write!(f, ", ")?;
5893 }
5894 write!(f, "{}", effect)?;
5895 }
5896 write!(f, "]")
5897 }
5898}
5899
5900#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
5902#[cfg_attr(feature = "serde", derive(Serialize, Deserialize))]
5903#[cfg_attr(feature = "schema", derive(JsonSchema))]
5904pub enum CompositionStrategy {
5905 FirstWins,
5907 LastWins,
5909 MostSpecific,
5911 AllApply,
5913}
5914
5915impl fmt::Display for CompositionStrategy {
5916 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
5917 match self {
5918 Self::FirstWins => write!(f, "FirstWins"),
5919 Self::LastWins => write!(f, "LastWins"),
5920 Self::MostSpecific => write!(f, "MostSpecific"),
5921 Self::AllApply => write!(f, "AllApply"),
5922 }
5923 }
5924}
5925
5926#[derive(Debug, Clone, PartialEq)]
5944#[cfg_attr(feature = "serde", derive(Serialize, Deserialize))]
5945#[cfg_attr(feature = "schema", derive(JsonSchema))]
5946pub struct TemporalEffect {
5947 pub effect: Effect,
5949 pub start_date: NaiveDate,
5951 pub end_date: Option<NaiveDate>,
5953 pub recurrence: Option<RecurrencePattern>,
5955}
5956
5957impl TemporalEffect {
5958 #[must_use]
5960 pub fn new(
5961 effect: Effect,
5962 start_date: NaiveDate,
5963 end_date: Option<NaiveDate>,
5964 recurrence: Option<RecurrencePattern>,
5965 ) -> Self {
5966 Self {
5967 effect,
5968 start_date,
5969 end_date,
5970 recurrence,
5971 }
5972 }
5973
5974 #[must_use]
5976 pub fn is_active_on(&self, date: NaiveDate) -> bool {
5977 if date < self.start_date {
5979 return false;
5980 }
5981 if let Some(end) = self.end_date
5982 && date > end
5983 {
5984 return false;
5985 }
5986
5987 if let Some(ref pattern) = self.recurrence {
5989 pattern.matches(date, self.start_date)
5990 } else {
5991 true
5992 }
5993 }
5994
5995 #[must_use]
5997 pub fn next_activation(&self, after: NaiveDate) -> Option<NaiveDate> {
5998 if let Some(ref pattern) = self.recurrence {
5999 pattern.next_occurrence(after, self.start_date, self.end_date)
6000 } else if after < self.start_date {
6001 Some(self.start_date)
6002 } else {
6003 None
6004 }
6005 }
6006}
6007
6008impl fmt::Display for TemporalEffect {
6009 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
6010 write!(f, "{} (active from {}", self.effect, self.start_date)?;
6011 if let Some(end) = self.end_date {
6012 write!(f, " to {}", end)?;
6013 }
6014 if let Some(ref rec) = self.recurrence {
6015 write!(f, ", {}", rec)?;
6016 }
6017 write!(f, ")")
6018 }
6019}
6020
6021#[derive(Debug, Clone, PartialEq, Eq)]
6023#[cfg_attr(feature = "serde", derive(Serialize, Deserialize))]
6024#[cfg_attr(feature = "schema", derive(JsonSchema))]
6025pub enum RecurrencePattern {
6026 Daily,
6028 Weekly { interval: u32 },
6030 Monthly { interval: u32 },
6032 Yearly { interval: u32 },
6034 DaysOfWeek { days: Vec<u32> },
6036 Custom { description: String },
6038}
6039
6040impl RecurrencePattern {
6041 #[must_use]
6043 pub fn matches(&self, date: NaiveDate, start: NaiveDate) -> bool {
6044 match self {
6045 Self::Daily => true,
6046 Self::Weekly { interval } => {
6047 let days_diff = (date - start).num_days();
6048 days_diff >= 0 && days_diff % ((*interval as i64) * 7) == 0
6049 }
6050 Self::Monthly { interval } => {
6051 let months_diff = (date.year() - start.year()) * 12
6052 + (date.month() as i32 - start.month() as i32);
6053 months_diff >= 0
6054 && months_diff % (*interval as i32) == 0
6055 && date.day() == start.day()
6056 }
6057 Self::Yearly { interval } => {
6058 let years_diff = date.year() - start.year();
6059 years_diff >= 0
6060 && years_diff % (*interval as i32) == 0
6061 && date.month() == start.month()
6062 && date.day() == start.day()
6063 }
6064 Self::DaysOfWeek { days } => {
6065 let weekday = date.weekday().num_days_from_sunday();
6066 days.contains(&weekday)
6067 }
6068 Self::Custom { .. } => true, }
6070 }
6071
6072 #[must_use]
6074 pub fn next_occurrence(
6075 &self,
6076 after: NaiveDate,
6077 start: NaiveDate,
6078 end: Option<NaiveDate>,
6079 ) -> Option<NaiveDate> {
6080 let mut candidate = after.succ_opt()?;
6081
6082 for _ in 0..365 {
6084 if let Some(end_date) = end
6085 && candidate > end_date
6086 {
6087 return None;
6088 }
6089 if candidate >= start && self.matches(candidate, start) {
6090 return Some(candidate);
6091 }
6092 candidate = candidate.succ_opt()?;
6093 }
6094 None
6095 }
6096}
6097
6098impl fmt::Display for RecurrencePattern {
6099 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
6100 match self {
6101 Self::Daily => write!(f, "daily"),
6102 Self::Weekly { interval } => write!(f, "every {} week(s)", interval),
6103 Self::Monthly { interval } => write!(f, "every {} month(s)", interval),
6104 Self::Yearly { interval } => write!(f, "every {} year(s)", interval),
6105 Self::DaysOfWeek { days } => {
6106 write!(f, "on days: ")?;
6107 for (i, day) in days.iter().enumerate() {
6108 if i > 0 {
6109 write!(f, ", ")?;
6110 }
6111 write!(f, "{}", day)?;
6112 }
6113 Ok(())
6114 }
6115 Self::Custom { description } => write!(f, "custom: {}", description),
6116 }
6117 }
6118}
6119
6120#[derive(Debug, Clone, PartialEq)]
6140#[cfg_attr(feature = "serde", derive(Serialize, Deserialize))]
6141pub struct ConditionalEffect {
6142 pub effect: Effect,
6144 pub condition: Condition,
6146}
6147
6148impl ConditionalEffect {
6149 #[must_use]
6151 pub fn new(effect: Effect, condition: Condition) -> Self {
6152 Self { effect, condition }
6153 }
6154
6155 pub fn should_apply<C: EvaluationContext>(&self, context: &C) -> Result<bool, EvaluationError> {
6157 self.condition.evaluate(context)
6158 }
6159
6160 pub fn apply_if<C: EvaluationContext>(
6162 &self,
6163 context: &C,
6164 ) -> Result<Option<&Effect>, EvaluationError> {
6165 if self.should_apply(context)? {
6166 Ok(Some(&self.effect))
6167 } else {
6168 Ok(None)
6169 }
6170 }
6171}
6172
6173impl fmt::Display for ConditionalEffect {
6174 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
6175 write!(f, "{} WHEN {}", self.effect, self.condition)
6176 }
6177}
6178
6179#[derive(Debug, Clone)]
6203pub struct EffectDependencyGraph {
6204 effects: std::collections::HashMap<String, Effect>,
6206 dependencies: std::collections::HashMap<String, Vec<String>>,
6208}
6209
6210impl EffectDependencyGraph {
6211 #[must_use]
6213 pub fn new() -> Self {
6214 Self {
6215 effects: std::collections::HashMap::new(),
6216 dependencies: std::collections::HashMap::new(),
6217 }
6218 }
6219
6220 pub fn add_effect(&mut self, id: String, effect: Effect) {
6222 self.effects.insert(id.clone(), effect);
6223 self.dependencies.entry(id).or_default();
6224 }
6225
6226 pub fn add_dependency(&mut self, from: &str, to: &str) -> Result<(), String> {
6230 if !self.effects.contains_key(from) {
6231 return Err(format!("Effect '{}' not found", from));
6232 }
6233 if !self.effects.contains_key(to) {
6234 return Err(format!("Effect '{}' not found", to));
6235 }
6236
6237 self.dependencies
6239 .entry(from.to_string())
6240 .or_default()
6241 .push(to.to_string());
6242
6243 if self.has_cycle() {
6245 if let Some(deps) = self.dependencies.get_mut(from) {
6247 deps.retain(|d| d != to);
6248 }
6249 return Err(format!(
6250 "Adding dependency {} -> {} would create a cycle",
6251 from, to
6252 ));
6253 }
6254
6255 Ok(())
6256 }
6257
6258 #[must_use]
6260 pub fn has_cycle(&self) -> bool {
6261 let mut visited = std::collections::HashSet::new();
6262 let mut rec_stack = std::collections::HashSet::new();
6263
6264 for node in self.effects.keys() {
6265 if self.has_cycle_util(node, &mut visited, &mut rec_stack) {
6266 return true;
6267 }
6268 }
6269 false
6270 }
6271
6272 fn has_cycle_util(
6274 &self,
6275 node: &str,
6276 visited: &mut std::collections::HashSet<String>,
6277 rec_stack: &mut std::collections::HashSet<String>,
6278 ) -> bool {
6279 if rec_stack.contains(node) {
6280 return true;
6281 }
6282 if visited.contains(node) {
6283 return false;
6284 }
6285
6286 visited.insert(node.to_string());
6287 rec_stack.insert(node.to_string());
6288
6289 if let Some(deps) = self.dependencies.get(node) {
6290 for dep in deps {
6291 if self.has_cycle_util(dep, visited, rec_stack) {
6292 return true;
6293 }
6294 }
6295 }
6296
6297 rec_stack.remove(node);
6298 false
6299 }
6300
6301 #[must_use]
6305 pub fn topological_sort(&self) -> Option<Vec<String>> {
6306 if self.has_cycle() {
6307 return None;
6308 }
6309
6310 let mut visited = std::collections::HashSet::new();
6311 let mut stack = Vec::new();
6312
6313 for node in self.effects.keys() {
6314 if !visited.contains(node) {
6315 self.topological_sort_util(node, &mut visited, &mut stack);
6316 }
6317 }
6318
6319 Some(stack)
6321 }
6322
6323 fn topological_sort_util(
6325 &self,
6326 node: &str,
6327 visited: &mut std::collections::HashSet<String>,
6328 stack: &mut Vec<String>,
6329 ) {
6330 visited.insert(node.to_string());
6331
6332 if let Some(deps) = self.dependencies.get(node) {
6333 for dep in deps {
6334 if !visited.contains(dep) {
6335 self.topological_sort_util(dep, visited, stack);
6336 }
6337 }
6338 }
6339
6340 stack.push(node.to_string());
6341 }
6342
6343 #[must_use]
6345 pub fn get_effect(&self, id: &str) -> Option<&Effect> {
6346 self.effects.get(id)
6347 }
6348
6349 #[must_use]
6351 pub fn len(&self) -> usize {
6352 self.effects.len()
6353 }
6354
6355 #[must_use]
6357 pub fn is_empty(&self) -> bool {
6358 self.effects.is_empty()
6359 }
6360}
6361
6362impl Default for EffectDependencyGraph {
6363 fn default() -> Self {
6364 Self::new()
6365 }
6366}
6367
6368#[derive(Debug, Clone, PartialEq, Eq, Hash)]
6370#[cfg_attr(feature = "serde", derive(Serialize, Deserialize))]
6371#[cfg_attr(feature = "schema", derive(JsonSchema))]
6372pub enum EffectType {
6373 Grant,
6375 Revoke,
6377 Obligation,
6379 Prohibition,
6381 MonetaryTransfer,
6383 StatusChange,
6385 Custom,
6387}
6388
6389impl fmt::Display for EffectType {
6390 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
6391 match self {
6392 Self::Grant => write!(f, "GRANT"),
6393 Self::Revoke => write!(f, "REVOKE"),
6394 Self::Obligation => write!(f, "OBLIGATION"),
6395 Self::Prohibition => write!(f, "PROHIBITION"),
6396 Self::MonetaryTransfer => write!(f, "MONETARY_TRANSFER"),
6397 Self::StatusChange => write!(f, "STATUS_CHANGE"),
6398 Self::Custom => write!(f, "CUSTOM"),
6399 }
6400 }
6401}
6402
6403#[derive(Debug, Clone, Default, PartialEq)]
6428#[cfg_attr(feature = "serde", derive(Serialize, Deserialize))]
6429#[cfg_attr(feature = "schema", derive(JsonSchema))]
6430pub struct TemporalValidity {
6431 pub effective_date: Option<NaiveDate>,
6433 pub expiry_date: Option<NaiveDate>,
6435 pub enacted_at: Option<DateTime<Utc>>,
6437 pub amended_at: Option<DateTime<Utc>>,
6439}
6440
6441impl TemporalValidity {
6442 pub fn new() -> Self {
6444 Self::default()
6445 }
6446
6447 pub fn with_effective_date(mut self, date: NaiveDate) -> Self {
6449 self.effective_date = Some(date);
6450 self
6451 }
6452
6453 pub fn with_expiry_date(mut self, date: NaiveDate) -> Self {
6455 self.expiry_date = Some(date);
6456 self
6457 }
6458
6459 pub fn with_enacted_at(mut self, timestamp: DateTime<Utc>) -> Self {
6461 self.enacted_at = Some(timestamp);
6462 self
6463 }
6464
6465 pub fn with_amended_at(mut self, timestamp: DateTime<Utc>) -> Self {
6467 self.amended_at = Some(timestamp);
6468 self
6469 }
6470
6471 pub fn is_active(&self, as_of: NaiveDate) -> bool {
6473 let after_effective = self.effective_date.is_none_or(|d| as_of >= d);
6474 let before_expiry = self.expiry_date.is_none_or(|d| as_of <= d);
6475 after_effective && before_expiry
6476 }
6477
6478 #[must_use]
6480 pub fn has_effective_date(&self) -> bool {
6481 self.effective_date.is_some()
6482 }
6483
6484 #[must_use]
6486 pub fn has_expiry_date(&self) -> bool {
6487 self.expiry_date.is_some()
6488 }
6489
6490 #[must_use]
6492 pub fn is_enacted(&self) -> bool {
6493 self.enacted_at.is_some()
6494 }
6495
6496 #[must_use]
6498 pub fn is_amended(&self) -> bool {
6499 self.amended_at.is_some()
6500 }
6501
6502 #[must_use]
6504 pub fn has_expired(&self, as_of: NaiveDate) -> bool {
6505 self.expiry_date.is_some_and(|exp| as_of > exp)
6506 }
6507
6508 #[must_use]
6510 pub fn is_pending(&self, as_of: NaiveDate) -> bool {
6511 self.effective_date.is_some_and(|eff| as_of < eff)
6512 }
6513}
6514
6515impl fmt::Display for TemporalValidity {
6516 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
6517 match (&self.effective_date, &self.expiry_date) {
6518 (Some(eff), Some(exp)) => write!(f, "valid {} to {}", eff, exp),
6519 (Some(eff), None) => write!(f, "effective from {}", eff),
6520 (None, Some(exp)) => write!(f, "expires {}", exp),
6521 (None, None) => write!(f, "no temporal constraints"),
6522 }
6523 }
6524}
6525
6526#[derive(Debug, Clone, PartialEq)]
6610#[cfg_attr(feature = "serde", derive(Serialize, Deserialize))]
6611#[cfg_attr(feature = "schema", derive(JsonSchema))]
6612pub struct StatuteException {
6613 pub id: String,
6615 pub description: String,
6617 pub condition: Condition,
6619}
6620
6621impl StatuteException {
6622 #[must_use]
6636 pub fn new(
6637 id: impl Into<String>,
6638 description: impl Into<String>,
6639 condition: Condition,
6640 ) -> Self {
6641 Self {
6642 id: id.into(),
6643 description: description.into(),
6644 condition,
6645 }
6646 }
6647}
6648
6649impl std::fmt::Display for StatuteException {
6650 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
6651 write!(
6652 f,
6653 "Exception '{}': {} when {}",
6654 self.id, self.description, self.condition
6655 )
6656 }
6657}
6658
6659#[derive(Debug, Clone)]
6660#[cfg_attr(feature = "serde", derive(Serialize, Deserialize))]
6661#[cfg_attr(feature = "schema", derive(JsonSchema))]
6662pub struct Statute {
6663 pub id: String,
6665 pub title: String,
6667 pub preconditions: Vec<Condition>,
6669 pub effect: Effect,
6671 pub discretion_logic: Option<String>,
6673 pub temporal_validity: TemporalValidity,
6675 pub version: u32,
6677 pub jurisdiction: Option<String>,
6679 pub derives_from: Vec<String>,
6681 pub applies_to: Vec<String>,
6683 pub exceptions: Vec<StatuteException>,
6685}
6686
6687impl Statute {
6688 pub fn new(id: impl Into<String>, title: impl Into<String>, effect: Effect) -> Self {
6690 Self {
6691 id: id.into(),
6692 title: title.into(),
6693 preconditions: Vec::new(),
6694 effect,
6695 discretion_logic: None,
6696 temporal_validity: TemporalValidity::default(),
6697 version: 1,
6698 jurisdiction: None,
6699 derives_from: Vec::new(),
6700 applies_to: Vec::new(),
6701 exceptions: Vec::new(),
6702 }
6703 }
6704
6705 pub fn with_precondition(mut self, condition: Condition) -> Self {
6707 self.preconditions.push(condition);
6708 self
6709 }
6710
6711 pub fn with_discretion(mut self, logic: impl Into<String>) -> Self {
6713 self.discretion_logic = Some(logic.into());
6714 self
6715 }
6716
6717 pub fn with_temporal_validity(mut self, validity: TemporalValidity) -> Self {
6719 self.temporal_validity = validity;
6720 self
6721 }
6722
6723 pub fn with_version(mut self, version: u32) -> Self {
6725 self.version = version;
6726 self
6727 }
6728
6729 pub fn with_jurisdiction(mut self, jurisdiction: impl Into<String>) -> Self {
6731 self.jurisdiction = Some(jurisdiction.into());
6732 self
6733 }
6734
6735 pub fn with_derives_from(mut self, source_id: impl Into<String>) -> Self {
6748 self.derives_from.push(source_id.into());
6749 self
6750 }
6751
6752 pub fn with_applies_to(mut self, entity_type: impl Into<String>) -> Self {
6766 self.applies_to.push(entity_type.into());
6767 self
6768 }
6769
6770 pub fn with_exception(mut self, exception: StatuteException) -> Self {
6787 self.exceptions.push(exception);
6788 self
6789 }
6790
6791 pub fn is_active(&self, as_of: NaiveDate) -> bool {
6793 self.temporal_validity.is_active(as_of)
6794 }
6795
6796 #[must_use]
6798 pub fn precondition_count(&self) -> usize {
6799 self.preconditions.len()
6800 }
6801
6802 #[must_use]
6804 pub fn has_preconditions(&self) -> bool {
6805 !self.preconditions.is_empty()
6806 }
6807
6808 #[must_use]
6810 pub fn has_discretion(&self) -> bool {
6811 self.discretion_logic.is_some()
6812 }
6813
6814 #[must_use]
6816 pub fn has_jurisdiction(&self) -> bool {
6817 self.jurisdiction.is_some()
6818 }
6819
6820 pub fn preconditions(&self) -> &[Condition] {
6822 &self.preconditions
6823 }
6824
6825 #[must_use]
6838 pub fn is_derived(&self) -> bool {
6839 !self.derives_from.is_empty()
6840 }
6841
6842 pub fn derivation_sources(&self) -> &[String] {
6856 &self.derives_from
6857 }
6858
6859 #[must_use]
6873 pub fn applies_to_entity_type(&self, entity_type: &str) -> bool {
6874 self.applies_to.iter().any(|t| t == entity_type)
6875 }
6876
6877 #[must_use]
6894 pub fn has_entity_restrictions(&self) -> bool {
6895 !self.applies_to.is_empty()
6896 }
6897
6898 pub fn applicable_entity_types(&self) -> &[String] {
6900 &self.applies_to
6901 }
6902
6903 #[must_use]
6920 pub fn has_exceptions(&self) -> bool {
6921 !self.exceptions.is_empty()
6922 }
6923
6924 pub fn exception_list(&self) -> &[StatuteException] {
6926 &self.exceptions
6927 }
6928
6929 #[must_use]
6931 pub fn exception_count(&self) -> usize {
6932 self.exceptions.len()
6933 }
6934
6935 pub fn validate(&self) -> Vec<ValidationError> {
6937 let mut errors = Vec::new();
6938
6939 if self.id.is_empty() {
6941 errors.push(ValidationError::EmptyId);
6942 } else if !self.is_valid_id(&self.id) {
6943 errors.push(ValidationError::InvalidId(self.id.clone()));
6944 }
6945
6946 if self.title.is_empty() {
6948 errors.push(ValidationError::EmptyTitle);
6949 }
6950
6951 if let (Some(eff), Some(exp)) = (
6953 self.temporal_validity.effective_date,
6954 self.temporal_validity.expiry_date,
6955 ) && exp < eff
6956 {
6957 errors.push(ValidationError::ExpiryBeforeEffective {
6958 effective: eff,
6959 expiry: exp,
6960 });
6961 }
6962
6963 for (i, cond) in self.preconditions.iter().enumerate() {
6965 if let Some(err) = Self::validate_condition(cond) {
6966 errors.push(ValidationError::InvalidCondition {
6967 index: i,
6968 message: err,
6969 });
6970 }
6971 }
6972
6973 if self.effect.description.is_empty() {
6975 errors.push(ValidationError::EmptyEffectDescription);
6976 }
6977
6978 if self.version == 0 {
6980 errors.push(ValidationError::InvalidVersion);
6981 }
6982
6983 errors
6984 }
6985
6986 pub fn is_valid(&self) -> bool {
6988 self.validate().is_empty()
6989 }
6990
6991 pub fn validated(self) -> Result<Self, Vec<ValidationError>> {
6993 let errors = self.validate();
6994 if errors.is_empty() {
6995 Ok(self)
6996 } else {
6997 Err(errors)
6998 }
6999 }
7000
7001 fn is_valid_id(&self, id: &str) -> bool {
7003 !id.is_empty()
7004 && id
7005 .chars()
7006 .all(|c| c.is_alphanumeric() || c == '-' || c == '_')
7007 && id.chars().next().is_some_and(|c| c.is_alphabetic())
7008 }
7009
7010 fn validate_condition(condition: &Condition) -> Option<String> {
7012 match condition {
7013 Condition::Age { value, .. } => {
7014 if *value > 150 {
7015 Some(format!("Unrealistic age value: {}", value))
7016 } else {
7017 None
7018 }
7019 }
7020 Condition::And(left, right) | Condition::Or(left, right) => {
7021 Self::validate_condition(left).or_else(|| Self::validate_condition(right))
7022 }
7023 Condition::Not(inner) => Self::validate_condition(inner),
7024 Condition::ResidencyDuration { months, .. } => {
7025 if *months > 1200 {
7026 Some(format!("Unrealistic residency duration: {} months", months))
7027 } else {
7028 None
7029 }
7030 }
7031 _ => None,
7032 }
7033 }
7034
7035 #[must_use]
7064 pub fn subsumes(&self, other: &Self) -> bool {
7065 if self.effect.effect_type != other.effect.effect_type
7071 || self.effect.description != other.effect.description
7072 {
7073 return false;
7074 }
7075
7076 if self.jurisdiction != other.jurisdiction && self.jurisdiction.is_some() {
7078 return false;
7079 }
7080
7081 if self.preconditions.is_empty() {
7088 return true;
7089 }
7090
7091 if other.preconditions.is_empty() {
7093 return false;
7094 }
7095
7096 self.preconditions.len() <= other.preconditions.len()
7099 }
7100
7101 #[must_use]
7119 pub fn is_subsumed_by(&self, other: &Self) -> bool {
7120 other.subsumes(self)
7121 }
7122
7123 #[must_use]
7145 pub fn diff(&self, other: &Self) -> StatuteDiff {
7146 let mut changes = Vec::new();
7147
7148 if self.id != other.id {
7150 changes.push(StatuteChange::IdChanged {
7151 old: self.id.clone(),
7152 new: other.id.clone(),
7153 });
7154 }
7155
7156 if self.title != other.title {
7158 changes.push(StatuteChange::TitleChanged {
7159 old: self.title.clone(),
7160 new: other.title.clone(),
7161 });
7162 }
7163
7164 if self.effect != other.effect {
7166 changes.push(StatuteChange::EffectChanged {
7167 old: format!("{}", self.effect),
7168 new: format!("{}", other.effect),
7169 });
7170 }
7171
7172 if self.preconditions != other.preconditions {
7174 changes.push(StatuteChange::PreconditionsChanged {
7175 added: other
7176 .preconditions
7177 .len()
7178 .saturating_sub(self.preconditions.len()),
7179 removed: self
7180 .preconditions
7181 .len()
7182 .saturating_sub(other.preconditions.len()),
7183 });
7184 }
7185
7186 if self.temporal_validity != other.temporal_validity {
7188 changes.push(StatuteChange::TemporalValidityChanged);
7189 }
7190
7191 if self.version != other.version {
7193 changes.push(StatuteChange::VersionChanged {
7194 old: self.version,
7195 new: other.version,
7196 });
7197 }
7198
7199 if self.jurisdiction != other.jurisdiction {
7201 changes.push(StatuteChange::JurisdictionChanged {
7202 old: self.jurisdiction.clone(),
7203 new: other.jurisdiction.clone(),
7204 });
7205 }
7206
7207 StatuteDiff {
7208 statute_id: self.id.clone(),
7209 changes,
7210 }
7211 }
7212}
7213
7214#[derive(Debug, Clone, PartialEq)]
7216#[cfg_attr(feature = "serde", derive(Serialize, Deserialize))]
7217#[cfg_attr(feature = "schema", derive(JsonSchema))]
7218pub struct StatuteDiff {
7219 pub statute_id: String,
7221 pub changes: Vec<StatuteChange>,
7223}
7224
7225impl StatuteDiff {
7226 #[must_use]
7228 pub fn is_empty(&self) -> bool {
7229 self.changes.is_empty()
7230 }
7231
7232 #[must_use]
7234 pub fn change_count(&self) -> usize {
7235 self.changes.len()
7236 }
7237}
7238
7239#[derive(Debug, Clone, PartialEq)]
7241#[cfg_attr(feature = "serde", derive(Serialize, Deserialize))]
7242#[cfg_attr(feature = "schema", derive(JsonSchema))]
7243pub enum StatuteChange {
7244 IdChanged { old: String, new: String },
7246 TitleChanged { old: String, new: String },
7248 EffectChanged { old: String, new: String },
7250 PreconditionsChanged { added: usize, removed: usize },
7252 TemporalValidityChanged,
7254 VersionChanged { old: u32, new: u32 },
7256 JurisdictionChanged {
7258 old: Option<String>,
7259 new: Option<String>,
7260 },
7261}
7262
7263impl fmt::Display for StatuteDiff {
7264 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
7265 if self.is_empty() {
7266 return write!(f, "No changes for statute '{}'", self.statute_id);
7267 }
7268
7269 writeln!(f, "Changes for statute '{}':", self.statute_id)?;
7270 for (i, change) in self.changes.iter().enumerate() {
7271 writeln!(f, " {}. {}", i + 1, change)?;
7272 }
7273 Ok(())
7274 }
7275}
7276
7277impl fmt::Display for StatuteChange {
7278 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
7279 match self {
7280 Self::IdChanged { old, new } => write!(f, "ID: '{}' → '{}'", old, new),
7281 Self::TitleChanged { old, new } => write!(f, "Title: '{}' → '{}'", old, new),
7282 Self::EffectChanged { old, new } => write!(f, "Effect: {} → {}", old, new),
7283 Self::PreconditionsChanged { added, removed } => {
7284 write!(f, "Preconditions: +{} -{}", added, removed)
7285 }
7286 Self::TemporalValidityChanged => write!(f, "Temporal validity changed"),
7287 Self::VersionChanged { old, new } => write!(f, "Version: {} → {}", old, new),
7288 Self::JurisdictionChanged { old, new } => {
7289 write!(
7290 f,
7291 "Jurisdiction: {} → {}",
7292 old.as_deref().unwrap_or("None"),
7293 new.as_deref().unwrap_or("None")
7294 )
7295 }
7296 }
7297 }
7298}
7299
7300#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Hash)]
7302#[cfg_attr(feature = "serde", derive(Serialize, Deserialize))]
7303#[cfg_attr(feature = "schema", derive(JsonSchema))]
7304pub enum ErrorSeverity {
7305 Warning,
7307 Error,
7309 Critical,
7311}
7312
7313impl fmt::Display for ErrorSeverity {
7314 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
7315 match self {
7316 Self::Warning => write!(f, "WARNING"),
7317 Self::Error => write!(f, "ERROR"),
7318 Self::Critical => write!(f, "CRITICAL"),
7319 }
7320 }
7321}
7322
7323#[derive(Debug, Clone, PartialEq)]
7325#[cfg_attr(feature = "serde", derive(Serialize, Deserialize))]
7326#[cfg_attr(feature = "schema", derive(JsonSchema))]
7327pub enum ValidationError {
7328 EmptyId,
7330 InvalidId(String),
7332 EmptyTitle,
7334 ExpiryBeforeEffective {
7336 effective: NaiveDate,
7337 expiry: NaiveDate,
7338 },
7339 InvalidCondition { index: usize, message: String },
7341 EmptyEffectDescription,
7343 InvalidVersion,
7345}
7346
7347impl ValidationError {
7348 #[must_use]
7350 pub const fn error_code(&self) -> &'static str {
7351 match self {
7352 Self::EmptyId => "E001",
7353 Self::InvalidId(_) => "E002",
7354 Self::EmptyTitle => "E003",
7355 Self::ExpiryBeforeEffective { .. } => "E004",
7356 Self::InvalidCondition { .. } => "E005",
7357 Self::EmptyEffectDescription => "E006",
7358 Self::InvalidVersion => "E007",
7359 }
7360 }
7361
7362 #[must_use]
7364 pub const fn severity(&self) -> ErrorSeverity {
7365 match self {
7366 Self::EmptyId | Self::EmptyTitle | Self::EmptyEffectDescription => {
7367 ErrorSeverity::Critical
7368 }
7369 Self::InvalidId(_) | Self::InvalidVersion => ErrorSeverity::Error,
7370 Self::ExpiryBeforeEffective { .. } | Self::InvalidCondition { .. } => {
7371 ErrorSeverity::Warning
7372 }
7373 }
7374 }
7375
7376 #[must_use]
7378 pub fn suggestion(&self) -> Option<&str> {
7379 match self {
7380 Self::EmptyId => Some("Provide a non-empty ID for the statute"),
7381 Self::InvalidId(_) => {
7382 Some("Use only alphanumeric characters, hyphens, and underscores in IDs")
7383 }
7384 Self::EmptyTitle => Some("Provide a descriptive title for the statute"),
7385 Self::ExpiryBeforeEffective { .. } => {
7386 Some("Ensure the expiry date is after the effective date")
7387 }
7388 Self::InvalidCondition { .. } => {
7389 Some("Review and fix the condition, or remove it if not needed")
7390 }
7391 Self::EmptyEffectDescription => Some("Provide a description for the effect"),
7392 Self::InvalidVersion => Some("Version must be greater than 0"),
7393 }
7394 }
7395
7396 #[must_use]
7408 pub fn recovery_options(&self) -> Vec<String> {
7409 match self {
7410 Self::EmptyId => vec![
7411 "Generate a unique ID based on title".to_string(),
7412 "Use a UUID as the ID".to_string(),
7413 "Derive ID from jurisdiction and statute number".to_string(),
7414 ],
7415 Self::InvalidId(id) => vec![
7416 format!("Remove invalid characters from '{}'", id),
7417 "Replace spaces with hyphens or underscores".to_string(),
7418 "Start ID with a letter if it begins with a number".to_string(),
7419 ],
7420 Self::EmptyTitle => vec![
7421 "Add a descriptive title summarizing the statute".to_string(),
7422 "Use the statute ID as a temporary title".to_string(),
7423 ],
7424 Self::ExpiryBeforeEffective { effective, expiry } => vec![
7425 format!("Change expiry date to be after {}", effective),
7426 format!("Change effective date to be before {}", expiry),
7427 "Remove the expiry date if statute doesn't expire".to_string(),
7428 ],
7429 Self::InvalidCondition { index, message } => vec![
7430 format!("Fix condition at index {}: {}", index, message),
7431 format!("Remove condition at index {}", index),
7432 "Simplify the condition to avoid validation issues".to_string(),
7433 ],
7434 Self::EmptyEffectDescription => vec![
7435 "Add a description explaining what the effect does".to_string(),
7436 "Use the effect type as a default description".to_string(),
7437 ],
7438 Self::InvalidVersion => vec![
7439 "Set version to 1 for new statutes".to_string(),
7440 "Increment version number from previous version".to_string(),
7441 ],
7442 }
7443 }
7444
7445 #[must_use]
7459 pub fn try_auto_fix(&self) -> Option<(String, String)> {
7460 match self {
7461 Self::InvalidId(id) => {
7462 let fixed = id
7463 .chars()
7464 .map(|c| {
7465 if c.is_alphanumeric() || c == '-' || c == '_' {
7466 c
7467 } else if c.is_whitespace() {
7468 '-'
7469 } else {
7470 '_'
7471 }
7472 })
7473 .collect::<String>();
7474 Some((
7475 fixed,
7476 "Replaced invalid characters with hyphens/underscores".to_string(),
7477 ))
7478 }
7479 Self::InvalidVersion => Some((
7480 "1".to_string(),
7481 "Set version to 1 (default for new statutes)".to_string(),
7482 )),
7483 _ => None,
7484 }
7485 }
7486}
7487
7488#[derive(Debug, Clone, PartialEq)]
7490#[cfg_attr(feature = "serde", derive(Serialize, Deserialize))]
7491#[cfg_attr(feature = "schema", derive(JsonSchema))]
7492pub enum ConditionError {
7493 MissingAttribute { key: String },
7495 TypeMismatch { expected: String, actual: String },
7497 InvalidFormula { formula: String, error: String },
7499 PatternError { pattern: String, error: String },
7501 MaxDepthExceeded { max_depth: usize },
7503 Custom { message: String },
7505}
7506
7507impl ConditionError {
7508 #[must_use]
7510 pub const fn error_code(&self) -> &'static str {
7511 match self {
7512 Self::MissingAttribute { .. } => "C001",
7513 Self::TypeMismatch { .. } => "C002",
7514 Self::InvalidFormula { .. } => "C003",
7515 Self::PatternError { .. } => "C004",
7516 Self::MaxDepthExceeded { .. } => "C005",
7517 Self::Custom { .. } => "C999",
7518 }
7519 }
7520
7521 #[must_use]
7523 pub const fn severity(&self) -> ErrorSeverity {
7524 match self {
7525 Self::MissingAttribute { .. } | Self::TypeMismatch { .. } => ErrorSeverity::Error,
7526 Self::InvalidFormula { .. } | Self::PatternError { .. } => ErrorSeverity::Critical,
7527 Self::MaxDepthExceeded { .. } => ErrorSeverity::Critical,
7528 Self::Custom { .. } => ErrorSeverity::Error,
7529 }
7530 }
7531
7532 #[must_use]
7545 pub fn suggestion(&self) -> Option<String> {
7546 match self {
7547 Self::MissingAttribute { key } => Some(format!(
7548 "Add the '{}' attribute to the entity before evaluation",
7549 key
7550 )),
7551 Self::TypeMismatch { expected, actual } => Some(format!(
7552 "Convert the value from {} to {} or adjust the condition type",
7553 actual, expected
7554 )),
7555 Self::InvalidFormula { formula, error } => Some(format!(
7556 "Fix the formula '{}': {}. Check syntax and ensure all variables are defined.",
7557 formula, error
7558 )),
7559 Self::PatternError { pattern, error } => Some(format!(
7560 "Fix the regex pattern '{}': {}. Ensure the pattern is valid regex syntax.",
7561 pattern, error
7562 )),
7563 Self::MaxDepthExceeded { max_depth } => Some(format!(
7564 "Simplify the condition structure to reduce nesting below {} levels, or check for circular references",
7565 max_depth
7566 )),
7567 Self::Custom { .. } => None,
7568 }
7569 }
7570
7571 #[must_use]
7586 pub fn recovery_options(&self) -> Vec<String> {
7587 match self {
7588 Self::MissingAttribute { key } => vec![
7589 format!("Add '{}' to entity attributes", key),
7590 "Use default value for missing attribute".to_string(),
7591 "Make this condition optional".to_string(),
7592 ],
7593 Self::TypeMismatch { expected, actual } => vec![
7594 format!("Convert {} to {}", actual, expected),
7595 "Change condition to accept current type".to_string(),
7596 "Add type conversion in evaluation context".to_string(),
7597 ],
7598 Self::InvalidFormula { .. } => vec![
7599 "Fix formula syntax".to_string(),
7600 "Use simpler condition type instead of calculation".to_string(),
7601 "Define missing variables in context".to_string(),
7602 ],
7603 Self::PatternError { .. } => vec![
7604 "Fix regex syntax".to_string(),
7605 "Escape special regex characters".to_string(),
7606 "Use simpler string comparison instead".to_string(),
7607 ],
7608 Self::MaxDepthExceeded { .. } => vec![
7609 "Flatten nested conditions using normalization".to_string(),
7610 "Break complex condition into multiple simpler ones".to_string(),
7611 "Check for and remove circular condition references".to_string(),
7612 ],
7613 Self::Custom { .. } => vec![],
7614 }
7615 }
7616}
7617
7618impl fmt::Display for ConditionError {
7619 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
7620 match self {
7621 Self::MissingAttribute { key } => write!(f, "Missing attribute: {}", key),
7622 Self::TypeMismatch { expected, actual } => {
7623 write!(f, "Type mismatch: expected {}, got {}", expected, actual)
7624 }
7625 Self::InvalidFormula { formula, error } => {
7626 write!(f, "Invalid formula '{}': {}", formula, error)
7627 }
7628 Self::PatternError { pattern, error } => {
7629 write!(f, "Pattern error '{}': {}", pattern, error)
7630 }
7631 Self::MaxDepthExceeded { max_depth } => {
7632 write!(f, "Maximum evaluation depth ({}) exceeded", max_depth)
7633 }
7634 Self::Custom { message } => write!(f, "{}", message),
7635 }
7636 }
7637}
7638
7639impl std::error::Error for ConditionError {}
7640
7641impl fmt::Display for ValidationError {
7642 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
7643 match self {
7644 Self::EmptyId => write!(f, "Statute ID cannot be empty"),
7645 Self::InvalidId(id) => write!(
7646 f,
7647 "Invalid statute ID: '{}' (must start with letter, contain only alphanumeric/dash/underscore)",
7648 id
7649 ),
7650 Self::EmptyTitle => write!(f, "Statute title cannot be empty"),
7651 Self::ExpiryBeforeEffective { effective, expiry } => {
7652 write!(
7653 f,
7654 "Expiry date ({}) cannot be before effective date ({})",
7655 expiry, effective
7656 )
7657 }
7658 Self::InvalidCondition { index, message } => {
7659 write!(f, "Invalid condition at index {}: {}", index, message)
7660 }
7661 Self::EmptyEffectDescription => write!(f, "Effect description cannot be empty"),
7662 Self::InvalidVersion => write!(f, "Version must be greater than 0"),
7663 }
7664 }
7665}
7666
7667impl std::error::Error for ValidationError {}
7668
7669#[derive(Debug, Clone, PartialEq, Eq)]
7675#[cfg_attr(feature = "serde", derive(Serialize, Deserialize))]
7676#[cfg_attr(feature = "schema", derive(JsonSchema))]
7677pub struct SourceLocation {
7678 pub file: Option<String>,
7680 pub line: Option<usize>,
7682 pub column: Option<usize>,
7684 pub snippet: Option<String>,
7686}
7687
7688impl SourceLocation {
7689 #[must_use]
7691 pub fn new() -> Self {
7692 Self {
7693 file: None,
7694 line: None,
7695 column: None,
7696 snippet: None,
7697 }
7698 }
7699
7700 #[must_use]
7702 pub fn with_file(mut self, file: impl Into<String>) -> Self {
7703 self.file = Some(file.into());
7704 self
7705 }
7706
7707 #[must_use]
7709 pub const fn with_line(mut self, line: usize) -> Self {
7710 self.line = Some(line);
7711 self
7712 }
7713
7714 #[must_use]
7716 pub const fn with_column(mut self, column: usize) -> Self {
7717 self.column = Some(column);
7718 self
7719 }
7720
7721 #[must_use]
7723 pub fn with_snippet(mut self, snippet: impl Into<String>) -> Self {
7724 self.snippet = Some(snippet.into());
7725 self
7726 }
7727}
7728
7729impl Default for SourceLocation {
7730 fn default() -> Self {
7731 Self::new()
7732 }
7733}
7734
7735impl fmt::Display for SourceLocation {
7736 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
7737 if let Some(file) = &self.file {
7738 write!(f, "{}", file)?;
7739 if let Some(line) = self.line {
7740 write!(f, ":{}", line)?;
7741 if let Some(column) = self.column {
7742 write!(f, ":{}", column)?;
7743 }
7744 }
7745 } else if let Some(line) = self.line {
7746 write!(f, "line {}", line)?;
7747 if let Some(column) = self.column {
7748 write!(f, ":{}", column)?;
7749 }
7750 } else {
7751 write!(f, "unknown location")?;
7752 }
7753 Ok(())
7754 }
7755}
7756
7757#[derive(Debug, Clone, PartialEq, Eq)]
7759#[cfg_attr(feature = "serde", derive(Serialize, Deserialize))]
7760#[cfg_attr(feature = "schema", derive(JsonSchema))]
7761pub struct DiagnosticContext {
7762 pub location: Option<SourceLocation>,
7764 pub statute_id: Option<String>,
7766 pub condition: Option<String>,
7768 pub stack: Vec<String>,
7770 pub notes: Vec<String>,
7772 pub suggestions: Vec<String>,
7774}
7775
7776impl DiagnosticContext {
7777 #[must_use]
7779 pub fn new() -> Self {
7780 Self {
7781 location: None,
7782 statute_id: None,
7783 condition: None,
7784 stack: Vec::new(),
7785 notes: Vec::new(),
7786 suggestions: Vec::new(),
7787 }
7788 }
7789
7790 #[must_use]
7792 pub fn with_location(mut self, location: SourceLocation) -> Self {
7793 self.location = Some(location);
7794 self
7795 }
7796
7797 #[must_use]
7799 pub fn with_statute_id(mut self, id: impl Into<String>) -> Self {
7800 self.statute_id = Some(id.into());
7801 self
7802 }
7803
7804 #[must_use]
7806 pub fn with_condition(mut self, condition: impl Into<String>) -> Self {
7807 self.condition = Some(condition.into());
7808 self
7809 }
7810
7811 pub fn add_stack_frame(&mut self, frame: impl Into<String>) {
7813 self.stack.push(frame.into());
7814 }
7815
7816 pub fn add_note(&mut self, note: impl Into<String>) {
7818 self.notes.push(note.into());
7819 }
7820
7821 pub fn add_suggestion(&mut self, suggestion: impl Into<String>) {
7823 self.suggestions.push(suggestion.into());
7824 }
7825
7826 #[must_use]
7828 pub fn with_stack_frame(mut self, frame: impl Into<String>) -> Self {
7829 self.stack.push(frame.into());
7830 self
7831 }
7832
7833 #[must_use]
7835 pub fn with_note(mut self, note: impl Into<String>) -> Self {
7836 self.notes.push(note.into());
7837 self
7838 }
7839
7840 #[must_use]
7842 pub fn with_suggestion(mut self, suggestion: impl Into<String>) -> Self {
7843 self.suggestions.push(suggestion.into());
7844 self
7845 }
7846}
7847
7848impl Default for DiagnosticContext {
7849 fn default() -> Self {
7850 Self::new()
7851 }
7852}
7853
7854impl fmt::Display for DiagnosticContext {
7855 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
7856 if let Some(location) = &self.location {
7857 writeln!(f, "at {}", location)?;
7858 if let Some(snippet) = &location.snippet {
7859 writeln!(f, " {}", snippet)?;
7860 }
7861 }
7862
7863 if let Some(statute_id) = &self.statute_id {
7864 writeln!(f, "in statute: {}", statute_id)?;
7865 }
7866
7867 if let Some(condition) = &self.condition {
7868 writeln!(f, "condition: {}", condition)?;
7869 }
7870
7871 if !self.stack.is_empty() {
7872 writeln!(f, "\nStack trace:")?;
7873 for (i, frame) in self.stack.iter().enumerate() {
7874 writeln!(f, " {}: {}", i, frame)?;
7875 }
7876 }
7877
7878 if !self.notes.is_empty() {
7879 writeln!(f, "\nNotes:")?;
7880 for note in &self.notes {
7881 writeln!(f, " - {}", note)?;
7882 }
7883 }
7884
7885 if !self.suggestions.is_empty() {
7886 writeln!(f, "\nSuggestions:")?;
7887 for suggestion in &self.suggestions {
7888 writeln!(f, " - {}", suggestion)?;
7889 }
7890 }
7891
7892 Ok(())
7893 }
7894}
7895
7896#[derive(Debug, Clone, PartialEq)]
7898#[cfg_attr(feature = "serde", derive(Serialize, Deserialize))]
7899pub struct DiagnosticValidationError {
7900 pub error: ValidationError,
7902 pub context: DiagnosticContext,
7904}
7905
7906impl DiagnosticValidationError {
7907 #[must_use]
7909 pub fn new(error: ValidationError) -> Self {
7910 Self {
7911 error,
7912 context: DiagnosticContext::new(),
7913 }
7914 }
7915
7916 #[must_use]
7918 pub fn with_context(mut self, context: DiagnosticContext) -> Self {
7919 self.context = context;
7920 self
7921 }
7922
7923 #[must_use]
7925 pub fn error_code(&self) -> &str {
7926 self.error.error_code()
7927 }
7928
7929 #[must_use]
7931 pub fn severity(&self) -> ErrorSeverity {
7932 self.error.severity()
7933 }
7934
7935 #[must_use]
7937 pub fn suggestion(&self) -> Option<&str> {
7938 self.error.suggestion()
7939 }
7940}
7941
7942impl fmt::Display for DiagnosticValidationError {
7943 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
7944 writeln!(f, "Error [{}]: {}", self.error_code(), self.error)?;
7945 write!(f, "{}", self.context)?;
7946 Ok(())
7947 }
7948}
7949
7950impl std::error::Error for DiagnosticValidationError {}
7951
7952#[derive(Debug, Default)]
7981pub struct DiagnosticReporter {
7982 errors: Vec<DiagnosticValidationError>,
7983}
7984
7985impl DiagnosticReporter {
7986 #[must_use]
7988 pub fn new() -> Self {
7989 Self::default()
7990 }
7991
7992 pub fn add_error(&mut self, error: ValidationError, context: DiagnosticContext) {
7994 self.errors
7995 .push(DiagnosticValidationError { error, context });
7996 }
7997
7998 pub fn add_simple_error(&mut self, error: ValidationError) {
8000 self.errors.push(DiagnosticValidationError::new(error));
8001 }
8002
8003 #[must_use]
8005 pub fn error_count(&self) -> usize {
8006 self.errors.len()
8007 }
8008
8009 #[must_use]
8011 pub fn is_empty(&self) -> bool {
8012 self.errors.is_empty()
8013 }
8014
8015 #[must_use]
8017 pub fn has_errors(&self) -> bool {
8018 !self.errors.is_empty()
8019 }
8020
8021 #[must_use]
8023 pub fn errors(&self) -> &[DiagnosticValidationError] {
8024 &self.errors
8025 }
8026
8027 #[must_use]
8029 pub fn errors_with_severity(&self, severity: ErrorSeverity) -> Vec<&DiagnosticValidationError> {
8030 self.errors
8031 .iter()
8032 .filter(|e| e.severity() == severity)
8033 .collect()
8034 }
8035
8036 #[must_use]
8038 pub fn critical_errors(&self) -> Vec<&DiagnosticValidationError> {
8039 self.errors_with_severity(ErrorSeverity::Critical)
8040 }
8041
8042 pub fn clear(&mut self) {
8044 self.errors.clear();
8045 }
8046
8047 #[must_use]
8049 pub fn report(&self) -> String {
8050 if self.errors.is_empty() {
8051 return "No errors".to_string();
8052 }
8053
8054 let mut output = String::new();
8055 output.push_str(&format!("\n{} error(s) found:\n\n", self.errors.len()));
8056
8057 for (i, error) in self.errors.iter().enumerate() {
8058 output.push_str(&format!("{}. {}\n", i + 1, error));
8059 }
8060
8061 output
8062 }
8063
8064 #[must_use]
8066 pub fn summary(&self) -> String {
8067 let critical = self.critical_errors().len();
8068 let errors = self.errors_with_severity(ErrorSeverity::Error).len();
8069 let warnings = self.errors_with_severity(ErrorSeverity::Warning).len();
8070
8071 format!(
8072 "{} total ({} critical, {} errors, {} warnings)",
8073 self.error_count(),
8074 critical,
8075 errors,
8076 warnings
8077 )
8078 }
8079}
8080
8081impl fmt::Display for DiagnosticReporter {
8082 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
8083 write!(f, "{}", self.report())
8084 }
8085}
8086
8087impl fmt::Display for Statute {
8088 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
8089 writeln!(f, "STATUTE {}: \"{}\"", self.id, self.title)?;
8090 if let Some(ref jur) = self.jurisdiction {
8091 writeln!(f, " JURISDICTION: {}", jur)?;
8092 }
8093 writeln!(f, " VERSION: {}", self.version)?;
8094 writeln!(f, " {}", self.temporal_validity)?;
8095 if !self.preconditions.is_empty() {
8096 writeln!(f, " WHEN:")?;
8097 for cond in &self.preconditions {
8098 writeln!(f, " {}", cond)?;
8099 }
8100 }
8101 writeln!(f, " THEN: {}", self.effect)?;
8102 if let Some(ref disc) = self.discretion_logic {
8103 writeln!(f, " DISCRETION: {}", disc)?;
8104 }
8105 Ok(())
8106 }
8107}
8108
8109#[derive(Debug, Clone, PartialEq, Eq)]
8111#[cfg_attr(feature = "serde", derive(Serialize, Deserialize))]
8112#[cfg_attr(feature = "schema", derive(JsonSchema))]
8113pub enum ConflictResolution {
8114 FirstPrevails(ConflictReason),
8116 SecondPrevails(ConflictReason),
8118 NoConflict,
8120 Unresolvable(String),
8122}
8123
8124#[derive(Debug, Clone, PartialEq, Eq, PartialOrd, Ord, Hash)]
8126#[cfg_attr(feature = "serde", derive(Serialize, Deserialize))]
8127#[cfg_attr(feature = "schema", derive(JsonSchema))]
8128pub enum ConflictReason {
8129 TemporalPrecedence,
8131 Specificity,
8133 Hierarchy,
8135 ExplicitAmendment,
8137}
8138
8139impl fmt::Display for ConflictReason {
8140 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
8141 match self {
8142 Self::TemporalPrecedence => write!(f, "lex posterior (later law prevails)"),
8143 Self::Specificity => write!(f, "lex specialis (more specific law prevails)"),
8144 Self::Hierarchy => write!(f, "lex superior (higher authority prevails)"),
8145 Self::ExplicitAmendment => write!(f, "explicit amendment/repeal"),
8146 }
8147 }
8148}
8149
8150impl fmt::Display for ConflictResolution {
8151 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
8152 match self {
8153 Self::FirstPrevails(reason) => write!(f, "First statute prevails: {}", reason),
8154 Self::SecondPrevails(reason) => write!(f, "Second statute prevails: {}", reason),
8155 Self::NoConflict => write!(f, "No conflict - statutes are compatible"),
8156 Self::Unresolvable(msg) => write!(f, "Unresolvable conflict: {}", msg),
8157 }
8158 }
8159}
8160
8161pub struct StatuteConflictAnalyzer;
8166
8167impl StatuteConflictAnalyzer {
8168 pub fn resolve(first: &Statute, second: &Statute) -> ConflictResolution {
8200 if !Self::has_conflict(first, second) {
8202 return ConflictResolution::NoConflict;
8203 }
8204
8205 if let Some(resolution) = Self::check_temporal_precedence(first, second) {
8210 return resolution;
8211 }
8212
8213 if let Some(resolution) = Self::check_specificity(first, second) {
8215 return resolution;
8216 }
8217
8218 if let Some(resolution) = Self::check_hierarchy(first, second) {
8220 return resolution;
8221 }
8222
8223 ConflictResolution::Unresolvable(
8225 "Statutes conflict but resolution requires human judgment".to_string(),
8226 )
8227 }
8228
8229 fn has_conflict(first: &Statute, second: &Statute) -> bool {
8231 use EffectType::*;
8234
8235 matches!(
8236 (&first.effect.effect_type, &second.effect.effect_type),
8237 (Grant, Prohibition)
8238 | (Grant, Revoke)
8239 | (Prohibition, Grant)
8240 | (Revoke, Grant)
8241 | (Obligation, Prohibition)
8242 | (Prohibition, Obligation)
8243 )
8244 }
8245
8246 fn check_temporal_precedence(first: &Statute, second: &Statute) -> Option<ConflictResolution> {
8248 let first_date = first.temporal_validity.effective_date?;
8249 let second_date = second.temporal_validity.effective_date?;
8250
8251 if first_date > second_date {
8252 Some(ConflictResolution::FirstPrevails(
8253 ConflictReason::TemporalPrecedence,
8254 ))
8255 } else if second_date > first_date {
8256 Some(ConflictResolution::SecondPrevails(
8257 ConflictReason::TemporalPrecedence,
8258 ))
8259 } else {
8260 None }
8262 }
8263
8264 fn check_specificity(first: &Statute, second: &Statute) -> Option<ConflictResolution> {
8268 let first_specificity = Self::calculate_specificity(first);
8269 let second_specificity = Self::calculate_specificity(second);
8270
8271 if first_specificity > second_specificity {
8272 Some(ConflictResolution::FirstPrevails(
8273 ConflictReason::Specificity,
8274 ))
8275 } else if second_specificity > first_specificity {
8276 Some(ConflictResolution::SecondPrevails(
8277 ConflictReason::Specificity,
8278 ))
8279 } else {
8280 None }
8282 }
8283
8284 fn calculate_specificity(statute: &Statute) -> usize {
8286 statute
8287 .preconditions
8288 .iter()
8289 .map(|c| c.count_conditions())
8290 .sum()
8291 }
8292
8293 fn check_hierarchy(first: &Statute, second: &Statute) -> Option<ConflictResolution> {
8297 let first_level = Self::jurisdiction_level(&first.jurisdiction);
8298 let second_level = Self::jurisdiction_level(&second.jurisdiction);
8299
8300 if first_level > second_level {
8301 Some(ConflictResolution::FirstPrevails(ConflictReason::Hierarchy))
8302 } else if second_level > first_level {
8303 Some(ConflictResolution::SecondPrevails(
8304 ConflictReason::Hierarchy,
8305 ))
8306 } else {
8307 None }
8309 }
8310
8311 fn jurisdiction_level(jurisdiction: &Option<String>) -> u32 {
8315 jurisdiction.as_ref().map_or(0, |j| {
8316 if j.to_lowercase().contains("federal") || j.to_lowercase().contains("national") {
8317 3
8318 } else if j.to_lowercase().contains("state") || j.to_lowercase().contains("provincial")
8319 {
8320 2
8321 } else if j.to_lowercase().contains("local") || j.to_lowercase().contains("municipal") {
8322 1
8323 } else {
8324 if j.len() <= 3 && j.chars().all(|c| c.is_ascii_uppercase()) {
8326 3 } else if j.contains('-') {
8328 2 } else {
8330 0 }
8332 }
8333 })
8334 }
8335
8336 pub fn is_in_effect(statute: &Statute, date: NaiveDate) -> bool {
8338 statute.temporal_validity.is_active(date)
8339 }
8340
8341 pub fn resolve_conflicts_at_date(statutes: &[Statute], date: NaiveDate) -> Vec<&Statute> {
8345 let mut active: Vec<&Statute> = statutes
8347 .iter()
8348 .filter(|s| Self::is_in_effect(s, date))
8349 .collect();
8350
8351 active.sort_by(|a, b| {
8353 let date_cmp = b
8355 .temporal_validity
8356 .effective_date
8357 .cmp(&a.temporal_validity.effective_date);
8358 if date_cmp != std::cmp::Ordering::Equal {
8359 return date_cmp;
8360 }
8361
8362 let spec_cmp = Self::calculate_specificity(b).cmp(&Self::calculate_specificity(a));
8364 if spec_cmp != std::cmp::Ordering::Equal {
8365 return spec_cmp;
8366 }
8367
8368 Self::jurisdiction_level(&b.jurisdiction)
8370 .cmp(&Self::jurisdiction_level(&a.jurisdiction))
8371 });
8372
8373 active
8374 }
8375
8376 #[must_use]
8400 pub fn detect_contradictions(statutes: &[Statute]) -> Vec<Contradiction> {
8401 let mut contradictions = Vec::new();
8402
8403 for (i, statute_a) in statutes.iter().enumerate() {
8405 for statute_b in statutes.iter().skip(i + 1) {
8406 if Self::effects_contradict(&statute_a.effect, &statute_b.effect) {
8408 if Self::conditions_may_overlap(
8410 &statute_a.preconditions,
8411 &statute_b.preconditions,
8412 ) {
8413 contradictions.push(Contradiction {
8414 statute_a_id: statute_a.id.clone(),
8415 statute_b_id: statute_b.id.clone(),
8416 contradiction_type: ContradictionType::ConflictingEffects,
8417 description: format!(
8418 "Statute '{}' grants while '{}' revokes the same right",
8419 statute_a.id, statute_b.id
8420 ),
8421 severity: ErrorSeverity::Critical,
8422 });
8423 }
8424 }
8425
8426 if statute_a.preconditions == statute_b.preconditions
8428 && statute_a.effect.effect_type != statute_b.effect.effect_type
8429 {
8430 contradictions.push(Contradiction {
8431 statute_a_id: statute_a.id.clone(),
8432 statute_b_id: statute_b.id.clone(),
8433 contradiction_type: ContradictionType::IdenticalConditionsConflictingEffects,
8434 description: format!(
8435 "Statutes '{}' and '{}' have identical conditions but conflicting effects",
8436 statute_a.id, statute_b.id
8437 ),
8438 severity: ErrorSeverity::Critical,
8439 });
8440 }
8441 }
8442 }
8443
8444 contradictions
8445 }
8446
8447 fn effects_contradict(effect_a: &Effect, effect_b: &Effect) -> bool {
8449 matches!(
8451 (&effect_a.effect_type, &effect_b.effect_type),
8452 (EffectType::Grant, EffectType::Revoke) | (EffectType::Revoke, EffectType::Grant)
8453 ) && effect_a.description == effect_b.description
8454 }
8455
8456 #[allow(dead_code)]
8459 fn conditions_may_overlap(conds_a: &[Condition], conds_b: &[Condition]) -> bool {
8460 conds_a.is_empty() || conds_b.is_empty() || conds_a == conds_b
8462 }
8463}
8464
8465#[derive(Debug, Clone, PartialEq)]
8467#[cfg_attr(feature = "serde", derive(Serialize, Deserialize))]
8468#[cfg_attr(feature = "schema", derive(JsonSchema))]
8469pub struct Contradiction {
8470 pub statute_a_id: String,
8472 pub statute_b_id: String,
8474 pub contradiction_type: ContradictionType,
8476 pub description: String,
8478 pub severity: ErrorSeverity,
8480}
8481
8482#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
8484#[cfg_attr(feature = "serde", derive(Serialize, Deserialize))]
8485#[cfg_attr(feature = "schema", derive(JsonSchema))]
8486pub enum ContradictionType {
8487 ConflictingEffects,
8489 IdenticalConditionsConflictingEffects,
8491 CircularDependency,
8493 LogicalInconsistency,
8495}
8496
8497impl fmt::Display for Contradiction {
8498 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
8499 write!(
8500 f,
8501 "[{}] {} <-> {}: {}",
8502 self.severity, self.statute_a_id, self.statute_b_id, self.description
8503 )
8504 }
8505}
8506
8507impl fmt::Display for ContradictionType {
8508 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
8509 match self {
8510 Self::ConflictingEffects => write!(f, "Conflicting Effects"),
8511 Self::IdenticalConditionsConflictingEffects => {
8512 write!(f, "Identical Conditions, Conflicting Effects")
8513 }
8514 Self::CircularDependency => write!(f, "Circular Dependency"),
8515 Self::LogicalInconsistency => write!(f, "Logical Inconsistency"),
8516 }
8517 }
8518}
8519
8520#[derive(Debug, Clone)]
8542pub struct ConditionBuilder {
8543 conditions: Vec<Condition>,
8544 operation: ConditionOperation,
8545}
8546
8547#[derive(Debug, Clone)]
8548enum ConditionOperation {
8549 None,
8550 And,
8551 Or,
8552}
8553
8554impl ConditionBuilder {
8555 #[must_use]
8557 pub fn new() -> Self {
8558 Self {
8559 conditions: Vec::new(),
8560 operation: ConditionOperation::None,
8561 }
8562 }
8563
8564 #[must_use]
8576 pub fn age(mut self, operator: ComparisonOp, value: u32) -> Self {
8577 self.conditions.push(Condition::Age { operator, value });
8578 self
8579 }
8580
8581 #[must_use]
8583 pub fn income(mut self, operator: ComparisonOp, value: u64) -> Self {
8584 self.conditions.push(Condition::Income { operator, value });
8585 self
8586 }
8587
8588 #[must_use]
8590 pub fn has_attribute(mut self, attr: impl Into<String>) -> Self {
8591 self.conditions
8592 .push(Condition::HasAttribute { key: attr.into() });
8593 self
8594 }
8595
8596 #[must_use]
8598 pub fn attribute_equals(mut self, attr: impl Into<String>, value: impl Into<String>) -> Self {
8599 self.conditions.push(Condition::AttributeEquals {
8600 key: attr.into(),
8601 value: value.into(),
8602 });
8603 self
8604 }
8605
8606 #[must_use]
8608 pub fn custom(mut self, description: impl Into<String>) -> Self {
8609 self.conditions.push(Condition::Custom {
8610 description: description.into(),
8611 });
8612 self
8613 }
8614
8615 #[must_use]
8629 pub fn and(mut self) -> Self {
8630 self.operation = ConditionOperation::And;
8631 self
8632 }
8633
8634 #[must_use]
8636 pub fn or(mut self) -> Self {
8637 self.operation = ConditionOperation::Or;
8638 self
8639 }
8640
8641 #[must_use]
8646 pub fn build(self) -> Condition {
8647 if self.conditions.is_empty() {
8648 Condition::Custom {
8649 description: "true".to_string(),
8650 }
8651 } else if self.conditions.len() == 1 {
8652 self.conditions.into_iter().next().unwrap()
8653 } else {
8654 let mut result = self.conditions[0].clone();
8656 for cond in self.conditions.into_iter().skip(1) {
8657 result = match self.operation {
8658 ConditionOperation::And => Condition::And(Box::new(result), Box::new(cond)),
8659 ConditionOperation::Or => Condition::Or(Box::new(result), Box::new(cond)),
8660 ConditionOperation::None => Condition::And(Box::new(result), Box::new(cond)),
8661 };
8662 }
8663 result
8664 }
8665 }
8666}
8667
8668impl Default for ConditionBuilder {
8669 fn default() -> Self {
8670 Self::new()
8671 }
8672}
8673
8674#[derive(Debug, Clone)]
8694pub struct EffectBuilder {
8695 effect_type: Option<EffectType>,
8696 description: Option<String>,
8697 parameters: std::collections::HashMap<String, String>,
8698}
8699
8700impl EffectBuilder {
8701 #[must_use]
8703 pub fn new() -> Self {
8704 Self {
8705 effect_type: None,
8706 description: None,
8707 parameters: std::collections::HashMap::new(),
8708 }
8709 }
8710
8711 #[must_use]
8725 pub fn grant(description: impl Into<String>) -> Self {
8726 Self {
8727 effect_type: Some(EffectType::Grant),
8728 description: Some(description.into()),
8729 parameters: std::collections::HashMap::new(),
8730 }
8731 }
8732
8733 #[must_use]
8735 pub fn revoke(description: impl Into<String>) -> Self {
8736 Self {
8737 effect_type: Some(EffectType::Revoke),
8738 description: Some(description.into()),
8739 parameters: std::collections::HashMap::new(),
8740 }
8741 }
8742
8743 #[must_use]
8745 pub fn obligation(description: impl Into<String>) -> Self {
8746 Self {
8747 effect_type: Some(EffectType::Obligation),
8748 description: Some(description.into()),
8749 parameters: std::collections::HashMap::new(),
8750 }
8751 }
8752
8753 #[must_use]
8755 pub fn effect_type(mut self, effect_type: EffectType) -> Self {
8756 self.effect_type = Some(effect_type);
8757 self
8758 }
8759
8760 #[must_use]
8762 pub fn description(mut self, desc: impl Into<String>) -> Self {
8763 self.description = Some(desc.into());
8764 self
8765 }
8766
8767 #[must_use]
8784 pub fn parameter(mut self, key: impl Into<String>, value: impl Into<String>) -> Self {
8785 self.parameters.insert(key.into(), value.into());
8786 self
8787 }
8788
8789 #[must_use]
8795 pub fn build(self) -> Effect {
8796 Effect {
8797 effect_type: self.effect_type.expect("Effect type must be set"),
8798 description: self.description.expect("Description must be set"),
8799 parameters: self.parameters,
8800 }
8801 }
8802
8803 pub fn try_build(self) -> Result<Effect, String> {
8805 let effect_type = self.effect_type.ok_or("Effect type not set")?;
8806 let description = self.description.ok_or("Description not set")?;
8807 Ok(Effect {
8808 effect_type,
8809 description,
8810 parameters: self.parameters,
8811 })
8812 }
8813}
8814
8815impl Default for EffectBuilder {
8816 fn default() -> Self {
8817 Self::new()
8818 }
8819}
8820
8821#[derive(Debug, Clone)]
8844pub struct StatuteBuilder {
8845 id: Option<String>,
8846 title: Option<String>,
8847 effect: Option<Effect>,
8848 preconditions: Vec<Condition>,
8849 discretion_logic: Option<String>,
8850 temporal_validity: TemporalValidity,
8851 version: u32,
8852 jurisdiction: Option<String>,
8853 derives_from: Vec<String>,
8854 applies_to: Vec<String>,
8855 exceptions: Vec<StatuteException>,
8856 progressive_validation: bool,
8857 validation_errors: Vec<ValidationError>,
8858}
8859
8860impl StatuteBuilder {
8861 #[must_use]
8863 pub fn new() -> Self {
8864 Self {
8865 id: None,
8866 title: None,
8867 effect: None,
8868 preconditions: Vec::new(),
8869 discretion_logic: None,
8870 temporal_validity: TemporalValidity::default(),
8871 version: 1,
8872 jurisdiction: None,
8873 derives_from: Vec::new(),
8874 applies_to: Vec::new(),
8875 exceptions: Vec::new(),
8876 progressive_validation: false,
8877 validation_errors: Vec::new(),
8878 }
8879 }
8880
8881 #[must_use]
8905 pub fn from_template(template: &Statute) -> Self {
8906 Self {
8907 id: Some(template.id.clone()),
8908 title: Some(template.title.clone()),
8909 effect: Some(template.effect.clone()),
8910 preconditions: template.preconditions.clone(),
8911 discretion_logic: template.discretion_logic.clone(),
8912 temporal_validity: template.temporal_validity.clone(),
8913 version: template.version,
8914 jurisdiction: template.jurisdiction.clone(),
8915 derives_from: template.derives_from.clone(),
8916 applies_to: template.applies_to.clone(),
8917 exceptions: template.exceptions.clone(),
8918 progressive_validation: false,
8919 validation_errors: Vec::new(),
8920 }
8921 }
8922
8923 #[must_use]
8943 pub fn validate_progressive(mut self, enabled: bool) -> Self {
8944 self.progressive_validation = enabled;
8945 self
8946 }
8947
8948 #[must_use]
8950 pub fn id(mut self, id: impl Into<String>) -> Self {
8951 let id = id.into();
8952 if self.progressive_validation {
8953 if id.is_empty() {
8954 self.validation_errors.push(ValidationError::EmptyId);
8955 } else if !self.is_valid_id(&id) {
8956 self.validation_errors
8957 .push(ValidationError::InvalidId(id.clone()));
8958 }
8959 }
8960 self.id = Some(id);
8961 self
8962 }
8963
8964 #[must_use]
8966 pub fn title(mut self, title: impl Into<String>) -> Self {
8967 let title = title.into();
8968 if self.progressive_validation && title.is_empty() {
8969 self.validation_errors.push(ValidationError::EmptyTitle);
8970 }
8971 self.title = Some(title);
8972 self
8973 }
8974
8975 #[must_use]
8977 pub fn effect(mut self, effect: Effect) -> Self {
8978 if self.progressive_validation && effect.description.is_empty() {
8979 self.validation_errors
8980 .push(ValidationError::EmptyEffectDescription);
8981 }
8982 self.effect = Some(effect);
8983 self
8984 }
8985
8986 #[must_use]
8988 pub fn precondition(mut self, condition: Condition) -> Self {
8989 self.preconditions.push(condition);
8990 self
8991 }
8992
8993 #[must_use]
8995 pub fn discretion(mut self, logic: impl Into<String>) -> Self {
8996 self.discretion_logic = Some(logic.into());
8997 self
8998 }
8999
9000 #[must_use]
9002 pub fn temporal_validity(mut self, validity: TemporalValidity) -> Self {
9003 self.temporal_validity = validity;
9004 self
9005 }
9006
9007 #[must_use]
9009 pub fn version(mut self, version: u32) -> Self {
9010 if self.progressive_validation && version == 0 {
9011 self.validation_errors.push(ValidationError::InvalidVersion);
9012 }
9013 self.version = version;
9014 self
9015 }
9016
9017 #[must_use]
9019 pub fn jurisdiction(mut self, jurisdiction: impl Into<String>) -> Self {
9020 self.jurisdiction = Some(jurisdiction.into());
9021 self
9022 }
9023
9024 #[must_use]
9026 pub fn derives_from(mut self, source: impl Into<String>) -> Self {
9027 self.derives_from.push(source.into());
9028 self
9029 }
9030
9031 #[must_use]
9033 pub fn applies_to(mut self, entity_type: impl Into<String>) -> Self {
9034 self.applies_to.push(entity_type.into());
9035 self
9036 }
9037
9038 #[must_use]
9040 pub fn exception(mut self, exception: StatuteException) -> Self {
9041 self.exceptions.push(exception);
9042 self
9043 }
9044
9045 #[must_use]
9047 pub fn validation_errors(&self) -> &[ValidationError] {
9048 &self.validation_errors
9049 }
9050
9051 fn is_valid_id(&self, id: &str) -> bool {
9053 !id.is_empty()
9054 && id
9055 .chars()
9056 .all(|c| c.is_alphanumeric() || c == '-' || c == '_')
9057 && id.chars().next().is_some_and(|c| c.is_alphabetic())
9058 }
9059
9060 pub fn build(self) -> Result<Statute, Vec<ValidationError>> {
9062 let mut errors = self.validation_errors;
9063
9064 if self.id.is_none() {
9066 errors.push(ValidationError::EmptyId);
9067 }
9068 if self.title.is_none() {
9069 errors.push(ValidationError::EmptyTitle);
9070 }
9071 if self.effect.is_none() {
9072 errors.push(ValidationError::EmptyEffectDescription);
9073 }
9074
9075 if !errors.is_empty() {
9076 return Err(errors);
9077 }
9078
9079 let statute = Statute {
9080 id: self.id.unwrap(),
9081 title: self.title.unwrap(),
9082 effect: self.effect.unwrap(),
9083 preconditions: self.preconditions,
9084 discretion_logic: self.discretion_logic,
9085 temporal_validity: self.temporal_validity,
9086 version: self.version,
9087 jurisdiction: self.jurisdiction,
9088 derives_from: self.derives_from,
9089 applies_to: self.applies_to,
9090 exceptions: self.exceptions,
9091 };
9092
9093 let validation_errors = statute.validate();
9095 if !validation_errors.is_empty() {
9096 Err(validation_errors)
9097 } else {
9098 Ok(statute)
9099 }
9100 }
9101}
9102
9103impl Default for StatuteBuilder {
9104 fn default() -> Self {
9105 Self::new()
9106 }
9107}
9108
9109pub mod builder_states {
9118 #[derive(Debug, Clone, Copy)]
9120 pub struct NoId;
9121 #[derive(Debug, Clone, Copy)]
9123 pub struct HasId;
9124 #[derive(Debug, Clone, Copy)]
9126 pub struct NoTitle;
9127 #[derive(Debug, Clone, Copy)]
9129 pub struct HasTitle;
9130 #[derive(Debug, Clone, Copy)]
9132 pub struct NoEffect;
9133 #[derive(Debug, Clone, Copy)]
9135 pub struct HasEffect;
9136}
9137
9138use builder_states::*;
9139
9140#[derive(Debug, Clone)]
9179pub struct TypedStatuteBuilder<I, T, E> {
9180 id: Option<String>,
9181 title: Option<String>,
9182 effect: Option<Effect>,
9183 preconditions: Vec<Condition>,
9184 discretion_logic: Option<String>,
9185 temporal_validity: TemporalValidity,
9186 version: u32,
9187 jurisdiction: Option<String>,
9188 _phantom: std::marker::PhantomData<(I, T, E)>,
9189}
9190
9191impl TypedStatuteBuilder<NoId, NoTitle, NoEffect> {
9192 #[must_use]
9194 pub fn new() -> Self {
9195 Self {
9196 id: None,
9197 title: None,
9198 effect: None,
9199 preconditions: Vec::new(),
9200 discretion_logic: None,
9201 temporal_validity: TemporalValidity::default(),
9202 version: 1,
9203 jurisdiction: None,
9204 _phantom: std::marker::PhantomData,
9205 }
9206 }
9207}
9208
9209impl<T, E> TypedStatuteBuilder<NoId, T, E> {
9210 #[must_use]
9214 pub fn id(self, id: impl Into<String>) -> TypedStatuteBuilder<HasId, T, E> {
9215 TypedStatuteBuilder {
9216 id: Some(id.into()),
9217 title: self.title,
9218 effect: self.effect,
9219 preconditions: self.preconditions,
9220 discretion_logic: self.discretion_logic,
9221 temporal_validity: self.temporal_validity,
9222 version: self.version,
9223 jurisdiction: self.jurisdiction,
9224 _phantom: std::marker::PhantomData,
9225 }
9226 }
9227}
9228
9229impl<I, E> TypedStatuteBuilder<I, NoTitle, E> {
9230 #[must_use]
9234 pub fn title(self, title: impl Into<String>) -> TypedStatuteBuilder<I, HasTitle, E> {
9235 TypedStatuteBuilder {
9236 id: self.id,
9237 title: Some(title.into()),
9238 effect: self.effect,
9239 preconditions: self.preconditions,
9240 discretion_logic: self.discretion_logic,
9241 temporal_validity: self.temporal_validity,
9242 version: self.version,
9243 jurisdiction: self.jurisdiction,
9244 _phantom: std::marker::PhantomData,
9245 }
9246 }
9247}
9248
9249impl<I, T> TypedStatuteBuilder<I, T, NoEffect> {
9250 #[must_use]
9254 pub fn effect(self, effect: Effect) -> TypedStatuteBuilder<I, T, HasEffect> {
9255 TypedStatuteBuilder {
9256 id: self.id,
9257 title: self.title,
9258 effect: Some(effect),
9259 preconditions: self.preconditions,
9260 discretion_logic: self.discretion_logic,
9261 temporal_validity: self.temporal_validity,
9262 version: self.version,
9263 jurisdiction: self.jurisdiction,
9264 _phantom: std::marker::PhantomData,
9265 }
9266 }
9267}
9268
9269impl<I, T, E> TypedStatuteBuilder<I, T, E> {
9271 #[must_use]
9273 pub fn with_precondition(mut self, condition: Condition) -> Self {
9274 self.preconditions.push(condition);
9275 self
9276 }
9277
9278 #[must_use]
9280 pub fn with_discretion(mut self, logic: impl Into<String>) -> Self {
9281 self.discretion_logic = Some(logic.into());
9282 self
9283 }
9284
9285 #[must_use]
9287 pub fn with_temporal_validity(mut self, validity: TemporalValidity) -> Self {
9288 self.temporal_validity = validity;
9289 self
9290 }
9291
9292 #[must_use]
9294 pub fn with_version(mut self, version: u32) -> Self {
9295 self.version = version;
9296 self
9297 }
9298
9299 #[must_use]
9301 pub fn with_jurisdiction(mut self, jurisdiction: impl Into<String>) -> Self {
9302 self.jurisdiction = Some(jurisdiction.into());
9303 self
9304 }
9305}
9306
9307impl TypedStatuteBuilder<HasId, HasTitle, HasEffect> {
9309 #[must_use]
9314 pub fn build(self) -> Statute {
9315 Statute {
9316 id: self.id.expect("ID must be set"),
9317 title: self.title.expect("Title must be set"),
9318 effect: self.effect.expect("Effect must be set"),
9319 preconditions: self.preconditions,
9320 discretion_logic: self.discretion_logic,
9321 temporal_validity: self.temporal_validity,
9322 version: self.version,
9323 jurisdiction: self.jurisdiction,
9324 derives_from: Vec::new(),
9325 applies_to: Vec::new(),
9326 exceptions: Vec::new(),
9327 }
9328 }
9329}
9330
9331impl Default for TypedStatuteBuilder<NoId, NoTitle, NoEffect> {
9332 fn default() -> Self {
9333 Self::new()
9334 }
9335}
9336
9337pub trait Jurisdiction: std::fmt::Debug + Clone {
9346 fn code() -> &'static str;
9348}
9349
9350#[derive(Debug, Clone, Copy)]
9352pub struct US;
9353
9354impl Jurisdiction for US {
9355 fn code() -> &'static str {
9356 "US"
9357 }
9358}
9359
9360#[derive(Debug, Clone, Copy)]
9362pub struct UK;
9363
9364impl Jurisdiction for UK {
9365 fn code() -> &'static str {
9366 "UK"
9367 }
9368}
9369
9370#[derive(Debug, Clone, Copy)]
9372pub struct EU;
9373
9374impl Jurisdiction for EU {
9375 fn code() -> &'static str {
9376 "EU"
9377 }
9378}
9379
9380#[derive(Debug, Clone, Copy)]
9382pub struct California;
9383
9384impl Jurisdiction for California {
9385 fn code() -> &'static str {
9386 "US-CA"
9387 }
9388}
9389
9390#[derive(Debug, Clone, Copy)]
9392pub struct NewYork;
9393
9394impl Jurisdiction for NewYork {
9395 fn code() -> &'static str {
9396 "US-NY"
9397 }
9398}
9399
9400#[derive(Debug, Clone, Copy)]
9402pub struct AnyJurisdiction;
9403
9404impl Jurisdiction for AnyJurisdiction {
9405 fn code() -> &'static str {
9406 ""
9407 }
9408}
9409
9410#[derive(Debug, Clone)]
9438#[cfg_attr(feature = "serde", derive(Serialize, Deserialize))]
9439pub struct JurisdictionStatute<J: Jurisdiction> {
9440 statute: Statute,
9441 _phantom: std::marker::PhantomData<J>,
9442}
9443
9444impl<J: Jurisdiction> JurisdictionStatute<J> {
9445 #[must_use]
9456 pub fn new(mut statute: Statute) -> Self {
9457 if statute.jurisdiction.is_none() {
9459 statute.jurisdiction = Some(J::code().to_string());
9460 }
9461 Self {
9462 statute,
9463 _phantom: std::marker::PhantomData,
9464 }
9465 }
9466
9467 #[must_use]
9469 pub fn jurisdiction_code(&self) -> &'static str {
9470 J::code()
9471 }
9472
9473 #[must_use]
9475 pub fn statute(&self) -> &Statute {
9476 &self.statute
9477 }
9478
9479 #[must_use]
9481 pub fn into_statute(self) -> Statute {
9482 self.statute
9483 }
9484
9485 #[must_use]
9501 pub fn convert_to<K: Jurisdiction>(mut self) -> JurisdictionStatute<K> {
9502 self.statute.jurisdiction = Some(K::code().to_string());
9503 JurisdictionStatute {
9504 statute: self.statute,
9505 _phantom: std::marker::PhantomData,
9506 }
9507 }
9508}
9509
9510#[derive(Debug, Clone)]
9531#[cfg_attr(feature = "serde", derive(Serialize, Deserialize))]
9532pub struct JurisdictionStatuteRegistry<J: Jurisdiction> {
9533 statutes: Vec<JurisdictionStatute<J>>,
9534 _phantom: std::marker::PhantomData<J>,
9535}
9536
9537impl<J: Jurisdiction> JurisdictionStatuteRegistry<J> {
9538 #[must_use]
9540 pub fn new() -> Self {
9541 Self {
9542 statutes: Vec::new(),
9543 _phantom: std::marker::PhantomData,
9544 }
9545 }
9546
9547 pub fn add(&mut self, statute: JurisdictionStatute<J>) {
9549 self.statutes.push(statute);
9550 }
9551
9552 #[must_use]
9554 pub fn len(&self) -> usize {
9555 self.statutes.len()
9556 }
9557
9558 #[must_use]
9560 pub fn is_empty(&self) -> bool {
9561 self.statutes.is_empty()
9562 }
9563
9564 #[must_use]
9566 pub fn jurisdiction_code(&self) -> &'static str {
9567 J::code()
9568 }
9569
9570 pub fn iter(&self) -> impl Iterator<Item = &JurisdictionStatute<J>> {
9572 self.statutes.iter()
9573 }
9574
9575 #[must_use]
9577 pub fn find(&self, id: &str) -> Option<&JurisdictionStatute<J>> {
9578 self.statutes.iter().find(|s| s.statute().id == id)
9579 }
9580}
9581
9582impl<J: Jurisdiction> Default for JurisdictionStatuteRegistry<J> {
9583 fn default() -> Self {
9584 Self::new()
9585 }
9586}
9587
9588#[macro_export]
9605macro_rules! define_jurisdiction {
9606 (
9607 $(#[$meta:meta])*
9608 $name:ident => $code:expr
9609 ) => {
9610 $(#[$meta])*
9611 #[derive(Debug, Clone, Copy)]
9612 pub struct $name;
9613
9614 impl $crate::Jurisdiction for $name {
9615 fn code() -> &'static str {
9616 $code
9617 }
9618 }
9619 };
9620}
9621
9622#[macro_export]
9668macro_rules! define_custom_condition {
9669 (
9670 $(#[$meta:meta])*
9671 $name:ident {
9672 $($field:ident: $field_type:ty),* $(,)?
9673 }
9674 ) => {
9675 $(#[$meta])*
9676 #[derive(Debug, Clone, PartialEq)]
9677 pub struct $name {
9678 $(pub $field: $field_type,)*
9679 }
9680
9681 impl $name {
9682 #[must_use]
9684 pub fn new($($field: $field_type),*) -> Self {
9685 Self {
9686 $($field,)*
9687 }
9688 }
9689
9690 #[must_use]
9692 pub fn to_condition(&self) -> $crate::Condition {
9693 $crate::Condition::Custom {
9694 description: self.to_string(),
9695 }
9696 }
9697
9698 #[allow(dead_code)]
9702 pub fn evaluate<C: $crate::EvaluationContext>(
9703 &self,
9704 _context: &C,
9705 ) -> Result<bool, $crate::EvaluationError> {
9706 Err($crate::EvaluationError::Custom {
9709 message: format!(
9710 "Evaluation not implemented for custom condition type '{}'",
9711 stringify!($name)
9712 ),
9713 })
9714 }
9715 }
9716
9717 impl From<$name> for $crate::Condition {
9718 fn from(custom: $name) -> Self {
9719 custom.to_condition()
9720 }
9721 }
9722
9723 impl std::fmt::Display for $name {
9724 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
9725 write!(f, "{}(", stringify!($name))?;
9726 let mut first = true;
9727 $(
9728 if !first {
9729 write!(f, ", ")?;
9730 }
9731 write!(f, "{}: {:?}", stringify!($field), self.$field)?;
9732 first = false;
9733 )*
9734 write!(f, ")")
9735 }
9736 }
9737 };
9738}
9739
9740#[doc(hidden)]
9742#[macro_export]
9743macro_rules! statute_impl_add_conditions {
9744 ($statute:ident, age >= $age:expr, $($rest:tt)*) => {
9745 $statute = $statute.with_precondition(
9746 $crate::Condition::age($crate::ComparisonOp::GreaterOrEqual, $age)
9747 );
9748 statute_impl_add_conditions!($statute, $($rest)*);
9749 };
9750 ($statute:ident, age > $age:expr, $($rest:tt)*) => {
9751 $statute = $statute.with_precondition(
9752 $crate::Condition::age($crate::ComparisonOp::GreaterThan, $age)
9753 );
9754 statute_impl_add_conditions!($statute, $($rest)*);
9755 };
9756 ($statute:ident, age < $age:expr, $($rest:tt)*) => {
9757 $statute = $statute.with_precondition(
9758 $crate::Condition::age($crate::ComparisonOp::LessThan, $age)
9759 );
9760 statute_impl_add_conditions!($statute, $($rest)*);
9761 };
9762 ($statute:ident, age <= $age:expr, $($rest:tt)*) => {
9763 $statute = $statute.with_precondition(
9764 $crate::Condition::age($crate::ComparisonOp::LessOrEqual, $age)
9765 );
9766 statute_impl_add_conditions!($statute, $($rest)*);
9767 };
9768 ($statute:ident, income >= $income:expr, $($rest:tt)*) => {
9769 $statute = $statute.with_precondition(
9770 $crate::Condition::income($crate::ComparisonOp::GreaterOrEqual, $income)
9771 );
9772 statute_impl_add_conditions!($statute, $($rest)*);
9773 };
9774 ($statute:ident, income < $income:expr, $($rest:tt)*) => {
9775 $statute = $statute.with_precondition(
9776 $crate::Condition::income($crate::ComparisonOp::LessThan, $income)
9777 );
9778 statute_impl_add_conditions!($statute, $($rest)*);
9779 };
9780 ($statute:ident, has_attribute $attr:expr, $($rest:tt)*) => {
9781 $statute = $statute.with_precondition(
9782 $crate::Condition::has_attribute($attr)
9783 );
9784 statute_impl_add_conditions!($statute, $($rest)*);
9785 };
9786 ($statute:ident,) => {};
9787 ($statute:ident) => {};
9788}
9789
9790#[doc(hidden)]
9792#[macro_export]
9793macro_rules! statute_impl_add_exceptions {
9794 ($statute:ident, $($rest:tt)*) => {};
9797}
9798
9799#[macro_export]
9836macro_rules! statute {
9837 (
9839 id: $id:expr,
9840 title: $title:expr,
9841 effect: $effect_type:ident($effect_desc:expr)
9842 $(, jurisdiction: $jurisdiction:expr)?
9843 $(, version: $version:expr)?
9844 $(, discretion: $discretion:expr)?
9845 $(,)?
9846 ) => {{
9847 let mut statute = $crate::Statute::new(
9848 $id,
9849 $title,
9850 $crate::Effect::new(
9851 $crate::EffectType::$effect_type,
9852 $effect_desc
9853 )
9854 );
9855
9856 $(
9857 statute = statute.with_jurisdiction($jurisdiction);
9858 )?
9859 $(
9860 statute = statute.with_version($version);
9861 )?
9862 $(
9863 statute = statute.with_discretion($discretion);
9864 )?
9865
9866 statute
9867 }};
9868}
9869
9870#[cfg(test)]
9871mod tests {
9872 use super::*;
9873
9874 #[cfg(test)]
9876 mod proptests {
9877 use super::*;
9878 use proptest::prelude::*;
9879
9880 fn statute_id_strategy() -> impl Strategy<Value = String> {
9882 "[a-z][a-z0-9_-]{0,30}".prop_map(|s| s.to_string())
9883 }
9884
9885 fn comparison_op_strategy() -> impl Strategy<Value = ComparisonOp> {
9887 prop_oneof![
9888 Just(ComparisonOp::Equal),
9889 Just(ComparisonOp::NotEqual),
9890 Just(ComparisonOp::GreaterThan),
9891 Just(ComparisonOp::GreaterOrEqual),
9892 Just(ComparisonOp::LessThan),
9893 Just(ComparisonOp::LessOrEqual),
9894 ]
9895 }
9896
9897 fn age_strategy() -> impl Strategy<Value = u32> {
9899 0u32..150u32
9900 }
9901
9902 fn condition_strategy() -> impl Strategy<Value = Condition> {
9904 let leaf = prop_oneof![
9905 (comparison_op_strategy(), age_strategy()).prop_map(|(op, age)| Condition::Age {
9906 operator: op,
9907 value: age
9908 }),
9909 (comparison_op_strategy(), any::<u64>()).prop_map(|(op, income)| {
9910 Condition::Income {
9911 operator: op,
9912 value: income,
9913 }
9914 }),
9915 any::<String>().prop_map(|key| Condition::HasAttribute { key }),
9916 (any::<String>(), any::<String>())
9917 .prop_map(|(key, value)| Condition::AttributeEquals { key, value }),
9918 any::<String>().prop_map(|desc| Condition::Custom { description: desc }),
9919 ];
9920 leaf.prop_recursive(
9921 3, 16, 5, |inner| {
9925 prop_oneof![
9926 (inner.clone(), inner.clone())
9927 .prop_map(|(a, b)| Condition::And(Box::new(a), Box::new(b))),
9928 (inner.clone(), inner.clone())
9929 .prop_map(|(a, b)| Condition::Or(Box::new(a), Box::new(b))),
9930 inner.clone().prop_map(|c| Condition::Not(Box::new(c))),
9931 ]
9932 },
9933 )
9934 }
9935
9936 proptest! {
9937 #[test]
9938 fn test_legal_result_map_preserves_deterministic(value in any::<i32>()) {
9939 let result = LegalResult::Deterministic(value);
9940 let mapped = result.map(|x| x + 1);
9941 prop_assert!(mapped.is_deterministic());
9942 if let LegalResult::Deterministic(v) = mapped {
9943 prop_assert_eq!(v, value + 1);
9944 }
9945 }
9946
9947 #[test]
9948 fn test_legal_result_discretion_stays_discretion(issue in "\\PC+", hint in proptest::option::of("\\PC+")) {
9949 let result: LegalResult<i32> = LegalResult::JudicialDiscretion {
9950 issue: issue.clone(),
9951 context_id: Uuid::new_v4(),
9952 narrative_hint: hint.clone(),
9953 };
9954 let mapped = result.map(|x: i32| x + 1);
9955 prop_assert!(mapped.requires_discretion());
9956 }
9957
9958 #[test]
9959 fn test_comparison_op_display_roundtrip(op in comparison_op_strategy()) {
9960 let display = format!("{}", op);
9961 prop_assert!(!display.is_empty());
9962 prop_assert!(display.len() <= 2);
9963 }
9964
9965 #[test]
9966 fn test_condition_display_not_empty(cond in condition_strategy()) {
9967 let display = format!("{}", cond);
9968 prop_assert!(!display.is_empty());
9969 }
9970
9971 #[test]
9972 fn test_statute_id_validation(id in statute_id_strategy()) {
9973 let statute = Statute::new(
9974 id.clone(),
9975 "Test Statute",
9976 Effect::new(EffectType::Grant, "Test effect"),
9977 );
9978 let errors = statute.validate();
9980 prop_assert!(!errors.iter().any(|e| matches!(e, ValidationError::InvalidId(_))));
9981 }
9982
9983 #[test]
9984 fn test_temporal_validity_consistency(
9985 eff_days in 0i64..1000i64,
9986 exp_days in 0i64..1000i64
9987 ) {
9988 let base_date = NaiveDate::from_ymd_opt(2025, 1, 1).unwrap();
9989 let effective = base_date + chrono::Duration::days(eff_days);
9990 let expiry = base_date + chrono::Duration::days(exp_days);
9991
9992 let validity = TemporalValidity::new()
9993 .with_effective_date(effective)
9994 .with_expiry_date(expiry);
9995
9996 if effective <= expiry {
9998 prop_assert!(validity.is_active(effective));
9999 prop_assert!(validity.is_active(expiry));
10000 if eff_days < exp_days {
10001 let mid_date = base_date + chrono::Duration::days((eff_days + exp_days) / 2);
10002 prop_assert!(validity.is_active(mid_date));
10003 }
10004 }
10005 }
10006
10007 #[test]
10008 fn test_typed_entity_u32_roundtrip(key in "[a-z_]{1,20}", value in any::<u32>()) {
10009 let mut entity = TypedEntity::new();
10010 entity.set_u32(key.clone(), value);
10011 let retrieved = entity.get_u32(&key);
10012 prop_assert_eq!(retrieved, Ok(value));
10013 }
10014
10015 #[test]
10016 fn test_typed_entity_bool_roundtrip(key in "[a-z_]{1,20}", value in any::<bool>()) {
10017 let mut entity = TypedEntity::new();
10018 entity.set_bool(key.clone(), value);
10019 let retrieved = entity.get_bool(&key);
10020 prop_assert_eq!(retrieved, Ok(value));
10021 }
10022
10023 #[test]
10024 fn test_typed_entity_string_roundtrip(
10025 key in "[a-z_]{1,20}",
10026 value in "\\PC*"
10027 ) {
10028 let mut entity = TypedEntity::new();
10029 entity.set_string(key.clone(), value.clone());
10030 let retrieved = entity.get_string(&key);
10031 prop_assert_eq!(retrieved, Ok(value.as_str()));
10032 }
10033
10034 #[test]
10035 fn test_effect_parameters_preservation(
10036 key in "[a-z_]{1,10}",
10037 value in "\\PC{0,20}"
10038 ) {
10039 let effect = Effect::new(EffectType::Grant, "Test effect")
10040 .with_parameter(key.clone(), value.clone());
10041
10042 prop_assert_eq!(effect.parameters.get(&key).map(String::as_str), Some(value.as_str()));
10043 prop_assert_eq!(effect.parameters.len(), 1);
10044 }
10045
10046 #[test]
10047 fn test_statute_version_validation(version in 1u32..1000u32) {
10048 let statute = Statute::new(
10049 "test-statute",
10050 "Test Statute",
10051 Effect::new(EffectType::Grant, "Test"),
10052 ).with_version(version);
10053
10054 let errors = statute.validate();
10055 prop_assert!(!errors.iter().any(|e| matches!(e, ValidationError::InvalidVersion)));
10057 }
10058 }
10059
10060 proptest! {
10062 #[test]
10063 fn test_basic_entity_attribute_storage(
10064 key in "[a-z_]{1,20}",
10065 value in "\\PC{0,50}"
10066 ) {
10067 let mut entity = BasicEntity::new();
10068 entity.set_attribute(&key, value.clone());
10069 let retrieved = entity.get_attribute(&key);
10070 prop_assert_eq!(retrieved, Some(value));
10071 }
10072
10073 #[test]
10074 fn test_statute_builder_preserves_properties(
10075 id in statute_id_strategy(),
10076 title in "\\PC{1,100}",
10077 jurisdiction in proptest::option::of("[A-Z]{2,3}")
10078 ) {
10079 let statute = Statute::new(
10080 id.clone(),
10081 title.clone(),
10082 Effect::new(EffectType::Grant, "Test"),
10083 ).with_jurisdiction(jurisdiction.clone().unwrap_or_default());
10084
10085 prop_assert_eq!(statute.id, id);
10086 prop_assert_eq!(statute.title, title);
10087 }
10088
10089 #[test]
10090 fn test_legal_result_void_stays_void(reason in "\\PC+") {
10091 let result: LegalResult<i32> = LegalResult::Void { reason: reason.clone() };
10092 let mapped = result.map(|x| x * 2);
10093 prop_assert!(mapped.is_void());
10094 }
10095 }
10096 }
10097
10098 #[test]
10099 fn test_legal_result_deterministic() {
10100 let result: LegalResult<i32> = LegalResult::Deterministic(42);
10101 assert!(result.is_deterministic());
10102 assert!(!result.requires_discretion());
10103 assert!(!result.is_void());
10104 }
10105
10106 #[test]
10107 fn test_legal_result_discretion() {
10108 let result: LegalResult<i32> = LegalResult::JudicialDiscretion {
10109 issue: "test issue".to_string(),
10110 context_id: Uuid::new_v4(),
10111 narrative_hint: None,
10112 };
10113 assert!(!result.is_deterministic());
10114 assert!(result.requires_discretion());
10115 }
10116
10117 #[test]
10118 fn test_legal_result_map() {
10119 let result: LegalResult<i32> = LegalResult::Deterministic(21);
10120 let mapped = result.map(|x| x * 2);
10121 assert_eq!(mapped, LegalResult::Deterministic(42));
10122 }
10123
10124 #[test]
10125 fn test_legal_result_display() {
10126 let det: LegalResult<i32> = LegalResult::Deterministic(42);
10127 assert_eq!(format!("{}", det), "Deterministic(42)");
10128
10129 let disc: LegalResult<i32> = LegalResult::JudicialDiscretion {
10130 issue: "test issue".to_string(),
10131 context_id: Uuid::new_v4(),
10132 narrative_hint: Some("consider facts".to_string()),
10133 };
10134 assert!(format!("{}", disc).contains("test issue"));
10135 assert!(format!("{}", disc).contains("consider facts"));
10136 }
10137
10138 #[test]
10139 fn test_basic_entity() {
10140 let mut entity = BasicEntity::new();
10141 entity.set_attribute("age", "25".to_string());
10142 assert_eq!(entity.get_attribute("age"), Some("25".to_string()));
10143 assert_eq!(entity.get_attribute("nonexistent"), None);
10144 }
10145
10146 #[test]
10147 fn test_statute_builder() {
10148 let statute = Statute::new(
10149 "test-statute-1",
10150 "Test Statute",
10151 Effect::new(EffectType::Grant, "Grant test permission"),
10152 )
10153 .with_precondition(Condition::Age {
10154 operator: ComparisonOp::GreaterOrEqual,
10155 value: 18,
10156 })
10157 .with_discretion("Consider special circumstances");
10158
10159 assert_eq!(statute.id, "test-statute-1");
10160 assert_eq!(statute.preconditions.len(), 1);
10161 assert!(statute.discretion_logic.is_some());
10162 }
10163
10164 #[test]
10165 fn test_temporal_validity() {
10166 let today = NaiveDate::from_ymd_opt(2025, 6, 15).unwrap();
10167 let past = NaiveDate::from_ymd_opt(2024, 1, 1).unwrap();
10168 let future = NaiveDate::from_ymd_opt(2026, 12, 31).unwrap();
10169
10170 let validity = TemporalValidity::new()
10171 .with_effective_date(past)
10172 .with_expiry_date(future);
10173
10174 assert!(validity.is_active(today));
10175 assert!(!validity.is_active(NaiveDate::from_ymd_opt(2023, 1, 1).unwrap()));
10176 assert!(!validity.is_active(NaiveDate::from_ymd_opt(2027, 1, 1).unwrap()));
10177 }
10178
10179 #[test]
10180 fn test_statute_with_temporal_validity() {
10181 let statute = Statute::new(
10182 "sunset-test",
10183 "Sunset Test Act",
10184 Effect::new(EffectType::Grant, "Temporary grant"),
10185 )
10186 .with_temporal_validity(
10187 TemporalValidity::new()
10188 .with_effective_date(NaiveDate::from_ymd_opt(2025, 1, 1).unwrap())
10189 .with_expiry_date(NaiveDate::from_ymd_opt(2025, 12, 31).unwrap()),
10190 )
10191 .with_jurisdiction("US-CA");
10192
10193 assert!(statute.is_active(NaiveDate::from_ymd_opt(2025, 6, 1).unwrap()));
10194 assert!(!statute.is_active(NaiveDate::from_ymd_opt(2024, 12, 31).unwrap()));
10195 assert_eq!(statute.jurisdiction, Some("US-CA".to_string()));
10196 }
10197
10198 #[test]
10199 fn test_condition_display() {
10200 let age_cond = Condition::Age {
10201 operator: ComparisonOp::GreaterOrEqual,
10202 value: 18,
10203 };
10204 assert_eq!(format!("{}", age_cond), "age >= 18");
10205
10206 let and_cond = Condition::And(
10207 Box::new(Condition::Age {
10208 operator: ComparisonOp::GreaterOrEqual,
10209 value: 18,
10210 }),
10211 Box::new(Condition::Income {
10212 operator: ComparisonOp::LessThan,
10213 value: 50000,
10214 }),
10215 );
10216 assert!(format!("{}", and_cond).contains("AND"));
10217 }
10218
10219 #[test]
10220 fn test_geographic_condition() {
10221 let cond = Condition::Geographic {
10222 region_type: RegionType::State,
10223 region_id: "CA".to_string(),
10224 };
10225 assert!(format!("{}", cond).contains("State"));
10226 assert!(format!("{}", cond).contains("CA"));
10227 }
10228
10229 #[test]
10230 fn test_entity_relationship_condition() {
10231 let cond = Condition::EntityRelationship {
10232 relationship_type: RelationshipType::Employment,
10233 target_entity_id: Some("employer-123".to_string()),
10234 };
10235 assert!(format!("{}", cond).contains("Employment"));
10236 }
10237
10238 #[test]
10239 fn test_statute_display() {
10240 let statute = Statute::new(
10241 "display-test",
10242 "Display Test Act",
10243 Effect::new(EffectType::Grant, "Test grant"),
10244 )
10245 .with_precondition(Condition::Age {
10246 operator: ComparisonOp::GreaterOrEqual,
10247 value: 21,
10248 })
10249 .with_version(2)
10250 .with_jurisdiction("JP");
10251
10252 let display = format!("{}", statute);
10253 assert!(display.contains("display-test"));
10254 assert!(display.contains("Display Test Act"));
10255 assert!(display.contains("VERSION: 2"));
10256 assert!(display.contains("JP"));
10257 }
10258
10259 #[test]
10260 fn test_statute_validation_valid() {
10261 let statute = Statute::new(
10262 "valid-statute",
10263 "Valid Statute",
10264 Effect::new(EffectType::Grant, "Grant something"),
10265 );
10266
10267 assert!(statute.is_valid());
10268 assert!(statute.validate().is_empty());
10269 }
10270
10271 #[test]
10272 fn test_statute_validation_empty_id() {
10273 let mut statute = Statute::new("temp", "Test", Effect::new(EffectType::Grant, "Grant"));
10274 statute.id = String::new();
10275
10276 let errors = statute.validate();
10277 assert!(errors.iter().any(|e| matches!(e, ValidationError::EmptyId)));
10278 }
10279
10280 #[test]
10281 fn test_statute_validation_invalid_id() {
10282 let mut statute = Statute::new("temp", "Test", Effect::new(EffectType::Grant, "Grant"));
10283 statute.id = "123-invalid".to_string(); let errors = statute.validate();
10286 assert!(
10287 errors
10288 .iter()
10289 .any(|e| matches!(e, ValidationError::InvalidId(_)))
10290 );
10291 }
10292
10293 #[test]
10294 fn test_statute_validation_empty_title() {
10295 let mut statute = Statute::new("test-id", "temp", Effect::new(EffectType::Grant, "Grant"));
10296 statute.title = String::new();
10297
10298 let errors = statute.validate();
10299 assert!(
10300 errors
10301 .iter()
10302 .any(|e| matches!(e, ValidationError::EmptyTitle))
10303 );
10304 }
10305
10306 #[test]
10307 fn test_statute_validation_expiry_before_effective() {
10308 let statute = Statute::new(
10309 "temporal-error",
10310 "Temporal Error Statute",
10311 Effect::new(EffectType::Grant, "Grant"),
10312 )
10313 .with_temporal_validity(TemporalValidity {
10314 effective_date: Some(NaiveDate::from_ymd_opt(2025, 12, 31).unwrap()),
10315 expiry_date: Some(NaiveDate::from_ymd_opt(2024, 1, 1).unwrap()), enacted_at: None,
10317 amended_at: None,
10318 });
10319
10320 let errors = statute.validate();
10321 assert!(
10322 errors
10323 .iter()
10324 .any(|e| matches!(e, ValidationError::ExpiryBeforeEffective { .. }))
10325 );
10326 }
10327
10328 #[test]
10329 fn test_statute_validation_invalid_condition() {
10330 let statute = Statute::new(
10331 "age-error",
10332 "Age Error Statute",
10333 Effect::new(EffectType::Grant, "Grant"),
10334 )
10335 .with_precondition(Condition::Age {
10336 operator: ComparisonOp::GreaterOrEqual,
10337 value: 200, });
10339
10340 let errors = statute.validate();
10341 assert!(
10342 errors
10343 .iter()
10344 .any(|e| matches!(e, ValidationError::InvalidCondition { .. }))
10345 );
10346 }
10347
10348 #[test]
10349 fn test_statute_validation_zero_version() {
10350 let mut statute = Statute::new(
10351 "zero-version",
10352 "Zero Version Statute",
10353 Effect::new(EffectType::Grant, "Grant"),
10354 );
10355 statute.version = 0;
10356
10357 let errors = statute.validate();
10358 assert!(
10359 errors
10360 .iter()
10361 .any(|e| matches!(e, ValidationError::InvalidVersion))
10362 );
10363 }
10364
10365 #[test]
10366 fn test_statute_validated_method() {
10367 let valid_statute = Statute::new(
10368 "valid",
10369 "Valid Statute",
10370 Effect::new(EffectType::Grant, "Grant"),
10371 );
10372 assert!(valid_statute.validated().is_ok());
10373
10374 let mut invalid_statute = Statute::new(
10375 "invalid",
10376 "Invalid",
10377 Effect::new(EffectType::Grant, "Grant"),
10378 );
10379 invalid_statute.id = String::new();
10380 assert!(invalid_statute.validated().is_err());
10381 }
10382
10383 #[test]
10384 fn test_validation_error_display() {
10385 assert!(ValidationError::EmptyId.to_string().contains("empty"));
10386 assert!(ValidationError::EmptyTitle.to_string().contains("title"));
10387 assert!(
10388 ValidationError::InvalidVersion
10389 .to_string()
10390 .contains("Version")
10391 );
10392 }
10393
10394 #[test]
10395 fn test_typed_entity_basic_operations() {
10396 let mut entity = TypedEntity::new();
10397
10398 entity.set_u32("age", 25);
10400 entity.set_u64("income", 50000);
10401 entity.set_bool("is_citizen", true);
10402 entity.set_string("name", "Alice");
10403 entity.set_date("birth_date", NaiveDate::from_ymd_opt(1999, 1, 15).unwrap());
10404 entity.set_f64("tax_rate", 0.15);
10405
10406 assert_eq!(entity.get_u32("age").unwrap(), 25);
10408 assert_eq!(entity.get_u64("income").unwrap(), 50000);
10409 assert!(entity.get_bool("is_citizen").unwrap());
10410 assert_eq!(entity.get_string("name").unwrap(), "Alice");
10411 assert_eq!(
10412 entity.get_date("birth_date").unwrap(),
10413 NaiveDate::from_ymd_opt(1999, 1, 15).unwrap()
10414 );
10415 assert_eq!(entity.get_f64("tax_rate").unwrap(), 0.15);
10416
10417 assert!(entity.has_attribute("age"));
10419 assert!(!entity.has_attribute("nonexistent"));
10420 }
10421
10422 #[test]
10423 fn test_typed_entity_type_safety() {
10424 let mut entity = TypedEntity::new();
10425 entity.set_string("name", "Bob");
10426
10427 assert!(entity.get_u32("name").is_err());
10429
10430 assert!(entity.get_u32("missing").is_err());
10432 }
10433
10434 #[test]
10435 fn test_typed_entity_legal_entity_trait() {
10436 let mut entity = TypedEntity::new();
10437
10438 let _id = entity.id();
10440 assert!(entity.get_attribute("age").is_none());
10441
10442 entity.set_attribute("age", "30".to_string());
10444 assert_eq!(entity.get_attribute("age").unwrap(), "30");
10445
10446 assert_eq!(entity.get_u32("age").unwrap(), 30);
10448
10449 entity.set_attribute("active", "true".to_string());
10451 assert!(entity.get_bool("active").unwrap());
10452 }
10453
10454 #[test]
10455 fn test_typed_entity_backward_compatibility() {
10456 let mut entity = TypedEntity::new();
10457
10458 entity.set_attribute("age", "25".to_string());
10460 entity.set_attribute("income", "50000".to_string());
10461 entity.set_attribute("is_citizen", "true".to_string());
10462
10463 assert_eq!(entity.get_u32("age").unwrap(), 25);
10465 assert_eq!(entity.get_u64("income").unwrap(), 50000);
10466 assert!(entity.get_bool("is_citizen").unwrap());
10467
10468 assert_eq!(entity.get_attribute("age").unwrap(), "25");
10470 assert_eq!(entity.get_attribute("income").unwrap(), "50000");
10471 assert_eq!(entity.get_attribute("is_citizen").unwrap(), "true");
10472 }
10473
10474 #[test]
10475 fn test_typed_entity_attribute_value_conversions() {
10476 let mut entity = TypedEntity::new();
10477
10478 entity.set_typed("count", AttributeValue::U32(42));
10480 let val = entity.get_typed("count").unwrap();
10481 assert_eq!(val.as_u32().unwrap(), 42);
10482 assert_eq!(val.as_u64().unwrap(), 42); entity.set_attribute("registration_date", "2024-01-15".to_string());
10486 assert_eq!(
10487 entity.get_date("registration_date").unwrap(),
10488 NaiveDate::from_ymd_opt(2024, 1, 15).unwrap()
10489 );
10490 }
10491
10492 #[test]
10493 fn test_typed_entity_integration_with_condition() {
10494 let mut entity = TypedEntity::new();
10495 entity.set_u32("age", 25);
10496
10497 assert_eq!(entity.get_attribute("age").unwrap(), "25");
10499
10500 let age_from_trait = entity
10502 .get_attribute("age")
10503 .and_then(|v| v.parse::<u32>().ok());
10504 assert_eq!(age_from_trait, Some(25));
10505 }
10506
10507 #[test]
10508 fn test_condition_helpers() {
10509 let simple = Condition::age(ComparisonOp::GreaterOrEqual, 18);
10510 assert!(simple.is_simple());
10511 assert!(!simple.is_compound());
10512 assert_eq!(simple.count_conditions(), 1);
10513 assert_eq!(simple.depth(), 1);
10514
10515 let compound = simple
10516 .clone()
10517 .and(Condition::income(ComparisonOp::LessThan, 50000));
10518 assert!(compound.is_compound());
10519 assert!(!compound.is_simple());
10520 assert_eq!(compound.count_conditions(), 3); assert_eq!(compound.depth(), 2);
10522
10523 let negated = simple.clone().not();
10524 assert!(negated.is_negation());
10525 assert!(negated.is_compound());
10526 assert_eq!(negated.count_conditions(), 2); }
10528
10529 #[test]
10530 fn test_condition_constructors() {
10531 let age_cond = Condition::age(ComparisonOp::GreaterOrEqual, 21);
10532 assert!(matches!(age_cond, Condition::Age { value: 21, .. }));
10533
10534 let income_cond = Condition::income(ComparisonOp::LessThan, 100000);
10535 assert!(matches!(
10536 income_cond,
10537 Condition::Income { value: 100000, .. }
10538 ));
10539
10540 let attr_cond = Condition::has_attribute("license");
10541 assert!(matches!(attr_cond, Condition::HasAttribute { .. }));
10542
10543 let eq_cond = Condition::attribute_equals("status", "active");
10544 assert!(matches!(eq_cond, Condition::AttributeEquals { .. }));
10545
10546 let custom = Condition::custom("Complex eligibility check");
10547 assert!(matches!(custom, Condition::Custom { .. }));
10548 }
10549
10550 #[test]
10551 fn test_condition_combinators() {
10552 let c1 = Condition::age(ComparisonOp::GreaterOrEqual, 18);
10553 let c2 = Condition::income(ComparisonOp::LessThan, 50000);
10554 let c3 = Condition::has_attribute("citizenship");
10555
10556 let combined = c1.and(c2).or(c3);
10557 assert_eq!(combined.count_conditions(), 5); assert_eq!(combined.depth(), 3);
10559 }
10560
10561 #[test]
10562 fn test_comparison_op_inverse() {
10563 assert_eq!(ComparisonOp::Equal.inverse(), ComparisonOp::NotEqual);
10564 assert_eq!(ComparisonOp::NotEqual.inverse(), ComparisonOp::Equal);
10565 assert_eq!(
10566 ComparisonOp::GreaterThan.inverse(),
10567 ComparisonOp::LessOrEqual
10568 );
10569 assert_eq!(
10570 ComparisonOp::GreaterOrEqual.inverse(),
10571 ComparisonOp::LessThan
10572 );
10573 assert_eq!(
10574 ComparisonOp::LessThan.inverse(),
10575 ComparisonOp::GreaterOrEqual
10576 );
10577 assert_eq!(
10578 ComparisonOp::LessOrEqual.inverse(),
10579 ComparisonOp::GreaterThan
10580 );
10581 }
10582
10583 #[test]
10584 fn test_comparison_op_classification() {
10585 assert!(ComparisonOp::Equal.is_equality());
10586 assert!(ComparisonOp::NotEqual.is_equality());
10587 assert!(!ComparisonOp::GreaterThan.is_equality());
10588
10589 assert!(ComparisonOp::GreaterThan.is_ordering());
10590 assert!(ComparisonOp::LessThan.is_ordering());
10591 assert!(!ComparisonOp::Equal.is_ordering());
10592 }
10593
10594 #[test]
10595 fn test_effect_helpers() {
10596 let mut effect = Effect::new(EffectType::Grant, "Test effect")
10597 .with_parameter("key1", "value1")
10598 .with_parameter("key2", "value2");
10599
10600 assert_eq!(effect.parameter_count(), 2);
10601 assert!(effect.has_parameter("key1"));
10602 assert!(!effect.has_parameter("key3"));
10603 assert_eq!(effect.get_parameter("key1"), Some(&"value1".to_string()));
10604 assert_eq!(effect.get_parameter("key3"), None);
10605
10606 let removed = effect.remove_parameter("key1");
10607 assert_eq!(removed, Some("value1".to_string()));
10608 assert_eq!(effect.parameter_count(), 1);
10609 }
10610
10611 #[test]
10612 fn test_effect_constructors() {
10613 let grant = Effect::grant("Right to vote");
10614 assert_eq!(grant.effect_type, EffectType::Grant);
10615
10616 let revoke = Effect::revoke("Driving privileges");
10617 assert_eq!(revoke.effect_type, EffectType::Revoke);
10618
10619 let obligation = Effect::obligation("Pay taxes");
10620 assert_eq!(obligation.effect_type, EffectType::Obligation);
10621
10622 let prohibition = Effect::prohibition("Smoking in public");
10623 assert_eq!(prohibition.effect_type, EffectType::Prohibition);
10624 }
10625
10626 #[test]
10627 fn test_statute_helper_methods() {
10628 let statute = Statute::new("test-id", "Test Statute", Effect::grant("Test permission"))
10629 .with_precondition(Condition::age(ComparisonOp::GreaterOrEqual, 18))
10630 .with_precondition(Condition::has_attribute("citizenship"))
10631 .with_discretion("Consider special circumstances");
10632
10633 assert_eq!(statute.precondition_count(), 2);
10634 assert!(statute.has_preconditions());
10635 assert!(statute.has_discretion());
10636 assert!(!statute.has_jurisdiction());
10637
10638 let with_jurisdiction = statute.clone().with_jurisdiction("US");
10639 assert!(with_jurisdiction.has_jurisdiction());
10640
10641 let conditions = statute.preconditions();
10642 assert_eq!(conditions.len(), 2);
10643 }
10644
10645 #[test]
10646 fn test_temporal_validity_helpers() {
10647 use chrono::Utc;
10648
10649 let validity = TemporalValidity::new()
10650 .with_effective_date(NaiveDate::from_ymd_opt(2025, 1, 1).unwrap())
10651 .with_expiry_date(NaiveDate::from_ymd_opt(2030, 12, 31).unwrap())
10652 .with_enacted_at(Utc::now());
10653
10654 assert!(validity.has_effective_date());
10655 assert!(validity.has_expiry_date());
10656 assert!(validity.is_enacted());
10657 assert!(!validity.is_amended());
10658
10659 let test_date_active = NaiveDate::from_ymd_opt(2026, 6, 15).unwrap();
10660 let test_date_expired = NaiveDate::from_ymd_opt(2031, 1, 1).unwrap();
10661 let test_date_pending = NaiveDate::from_ymd_opt(2024, 12, 31).unwrap();
10662
10663 assert!(validity.is_active(test_date_active));
10664 assert!(validity.has_expired(test_date_expired));
10665 assert!(validity.is_pending(test_date_pending));
10666 }
10667
10668 #[test]
10669 fn test_duration_condition() {
10670 let employment = Condition::duration(ComparisonOp::GreaterOrEqual, 5, DurationUnit::Years);
10671 assert!(matches!(employment, Condition::Duration { .. }));
10672 assert_eq!(format!("{}", employment), "duration >= 5 years");
10673
10674 let probation = Condition::duration(ComparisonOp::LessThan, 90, DurationUnit::Days);
10675 assert_eq!(format!("{}", probation), "duration < 90 days");
10676 }
10677
10678 #[test]
10679 fn test_percentage_condition() {
10680 let ownership = Condition::percentage(ComparisonOp::GreaterOrEqual, 25, "ownership");
10681 assert!(matches!(ownership, Condition::Percentage { .. }));
10682 assert_eq!(format!("{}", ownership), "ownership >= 25%");
10683
10684 let threshold = Condition::percentage(ComparisonOp::LessThan, 50, "voting_power");
10685 assert_eq!(format!("{}", threshold), "voting_power < 50%");
10686 }
10687
10688 #[test]
10689 fn test_set_membership_condition() {
10690 let status_in = Condition::in_set(
10691 "status",
10692 vec![
10693 "active".to_string(),
10694 "pending".to_string(),
10695 "approved".to_string(),
10696 ],
10697 );
10698 assert!(matches!(status_in, Condition::SetMembership { .. }));
10699 assert_eq!(
10700 format!("{}", status_in),
10701 "status IN {active, pending, approved}"
10702 );
10703
10704 let status_not_in = Condition::not_in_set(
10705 "status",
10706 vec!["rejected".to_string(), "canceled".to_string()],
10707 );
10708 assert_eq!(
10709 format!("{}", status_not_in),
10710 "status NOT IN {rejected, canceled}"
10711 );
10712 }
10713
10714 #[test]
10715 fn test_pattern_condition() {
10716 let matches = Condition::matches_pattern("id", "^[A-Z]{2}[0-9]{6}$");
10717 assert!(matches!(matches, Condition::Pattern { .. }));
10718 assert_eq!(format!("{}", matches), "id =~ /^[A-Z]{2}[0-9]{6}$/");
10719
10720 let not_matches = Condition::not_matches_pattern("email", ".*@spam\\.com$");
10721 assert_eq!(format!("{}", not_matches), "email !~ /.*@spam\\.com$/");
10722 }
10723
10724 #[test]
10725 fn test_duration_unit_display() {
10726 assert_eq!(format!("{}", DurationUnit::Days), "days");
10727 assert_eq!(format!("{}", DurationUnit::Weeks), "weeks");
10728 assert_eq!(format!("{}", DurationUnit::Months), "months");
10729 assert_eq!(format!("{}", DurationUnit::Years), "years");
10730 }
10731
10732 #[test]
10733 fn test_duration_unit_ordering() {
10734 assert!(DurationUnit::Days < DurationUnit::Weeks);
10735 assert!(DurationUnit::Weeks < DurationUnit::Months);
10736 assert!(DurationUnit::Months < DurationUnit::Years);
10737 }
10738
10739 #[test]
10740 fn test_new_conditions_with_combinators() {
10741 let employment_eligible =
10742 Condition::duration(ComparisonOp::GreaterOrEqual, 1, DurationUnit::Years).and(
10743 Condition::percentage(ComparisonOp::GreaterOrEqual, 80, "attendance"),
10744 );
10745
10746 assert!(format!("{}", employment_eligible).contains("AND"));
10747 assert!(format!("{}", employment_eligible).contains("duration"));
10748 assert!(format!("{}", employment_eligible).contains("attendance"));
10749
10750 let status_check =
10751 Condition::in_set("status", vec!["active".to_string(), "verified".to_string()])
10752 .or(Condition::matches_pattern("id", "^VIP-"));
10753
10754 assert!(format!("{}", status_check).contains("OR"));
10755 assert!(format!("{}", status_check).contains("IN"));
10756 assert!(format!("{}", status_check).contains("=~"));
10757 }
10758
10759 #[test]
10760 fn test_new_conditions_count_and_depth() {
10761 let simple = Condition::duration(ComparisonOp::GreaterOrEqual, 5, DurationUnit::Years);
10762 assert_eq!(simple.count_conditions(), 1);
10763 assert_eq!(simple.depth(), 1);
10764 assert!(simple.is_simple());
10765 assert!(!simple.is_compound());
10766
10767 let compound = Condition::percentage(ComparisonOp::GreaterOrEqual, 25, "ownership").and(
10768 Condition::in_set("status", vec!["active".to_string(), "verified".to_string()]),
10769 );
10770 assert_eq!(compound.count_conditions(), 3);
10771 assert_eq!(compound.depth(), 2);
10772 assert!(!compound.is_simple());
10773 assert!(compound.is_compound());
10774 }
10775
10776 #[test]
10777 fn test_conflict_resolution_temporal_precedence() {
10778 let old_law = Statute::new(
10779 "old-1",
10780 "Old Law",
10781 Effect::new(EffectType::Grant, "Old grant"),
10782 )
10783 .with_temporal_validity(
10784 TemporalValidity::new()
10785 .with_effective_date(NaiveDate::from_ymd_opt(2020, 1, 1).unwrap()),
10786 );
10787
10788 let new_law = Statute::new(
10789 "new-1",
10790 "New Law",
10791 Effect::new(EffectType::Prohibition, "New prohibition"),
10792 )
10793 .with_temporal_validity(
10794 TemporalValidity::new()
10795 .with_effective_date(NaiveDate::from_ymd_opt(2025, 1, 1).unwrap()),
10796 );
10797
10798 let resolution = StatuteConflictAnalyzer::resolve(&old_law, &new_law);
10799 assert_eq!(
10800 resolution,
10801 ConflictResolution::SecondPrevails(ConflictReason::TemporalPrecedence)
10802 );
10803 }
10804
10805 #[test]
10806 fn test_conflict_resolution_specificity() {
10807 let general = Statute::new(
10808 "general",
10809 "General Law",
10810 Effect::new(EffectType::Grant, "General grant"),
10811 )
10812 .with_temporal_validity(
10813 TemporalValidity::new()
10814 .with_effective_date(NaiveDate::from_ymd_opt(2020, 1, 1).unwrap()),
10815 );
10816
10817 let specific = Statute::new(
10818 "specific",
10819 "Specific Law",
10820 Effect::new(EffectType::Prohibition, "Specific prohibition"),
10821 )
10822 .with_precondition(Condition::age(ComparisonOp::GreaterOrEqual, 18))
10823 .with_precondition(Condition::income(ComparisonOp::LessThan, 50000))
10824 .with_temporal_validity(
10825 TemporalValidity::new()
10826 .with_effective_date(NaiveDate::from_ymd_opt(2020, 1, 1).unwrap()),
10827 );
10828
10829 let resolution = StatuteConflictAnalyzer::resolve(&general, &specific);
10830 assert_eq!(
10831 resolution,
10832 ConflictResolution::SecondPrevails(ConflictReason::Specificity)
10833 );
10834 }
10835
10836 #[test]
10837 fn test_conflict_resolution_hierarchy() {
10838 let state_law = Statute::new(
10839 "state-1",
10840 "State Law",
10841 Effect::new(EffectType::Grant, "State grant"),
10842 )
10843 .with_jurisdiction("US-NY")
10844 .with_temporal_validity(
10845 TemporalValidity::new()
10846 .with_effective_date(NaiveDate::from_ymd_opt(2020, 1, 1).unwrap()),
10847 );
10848
10849 let federal_law = Statute::new(
10850 "fed-1",
10851 "Federal Law",
10852 Effect::new(EffectType::Prohibition, "Federal prohibition"),
10853 )
10854 .with_jurisdiction("US")
10855 .with_temporal_validity(
10856 TemporalValidity::new()
10857 .with_effective_date(NaiveDate::from_ymd_opt(2020, 1, 1).unwrap()),
10858 );
10859
10860 let resolution = StatuteConflictAnalyzer::resolve(&state_law, &federal_law);
10861 assert_eq!(
10862 resolution,
10863 ConflictResolution::SecondPrevails(ConflictReason::Hierarchy)
10864 );
10865 }
10866
10867 #[test]
10868 fn test_conflict_resolution_no_conflict() {
10869 let law1 = Statute::new("law-1", "Law 1", Effect::new(EffectType::Grant, "Grant A"));
10870
10871 let law2 = Statute::new("law-2", "Law 2", Effect::new(EffectType::Grant, "Grant B"));
10872
10873 let resolution = StatuteConflictAnalyzer::resolve(&law1, &law2);
10874 assert_eq!(resolution, ConflictResolution::NoConflict);
10875 }
10876
10877 #[test]
10878 fn test_conflict_resolution_unresolvable() {
10879 let law1 = Statute::new("law-1", "Law 1", Effect::new(EffectType::Grant, "Grant"))
10881 .with_temporal_validity(
10882 TemporalValidity::new()
10883 .with_effective_date(NaiveDate::from_ymd_opt(2020, 1, 1).unwrap()),
10884 )
10885 .with_jurisdiction("US");
10886
10887 let law2 = Statute::new(
10888 "law-2",
10889 "Law 2",
10890 Effect::new(EffectType::Prohibition, "Prohibition"),
10891 )
10892 .with_temporal_validity(
10893 TemporalValidity::new()
10894 .with_effective_date(NaiveDate::from_ymd_opt(2020, 1, 1).unwrap()),
10895 )
10896 .with_jurisdiction("US");
10897
10898 let resolution = StatuteConflictAnalyzer::resolve(&law1, &law2);
10899 assert!(matches!(resolution, ConflictResolution::Unresolvable(_)));
10900 }
10901
10902 #[test]
10903 fn test_conflict_resolution_is_in_effect() {
10904 let statute = Statute::new("test", "Test", Effect::grant("Test")).with_temporal_validity(
10905 TemporalValidity::new()
10906 .with_effective_date(NaiveDate::from_ymd_opt(2020, 1, 1).unwrap())
10907 .with_expiry_date(NaiveDate::from_ymd_opt(2030, 12, 31).unwrap()),
10908 );
10909
10910 assert!(StatuteConflictAnalyzer::is_in_effect(
10911 &statute,
10912 NaiveDate::from_ymd_opt(2025, 6, 15).unwrap()
10913 ));
10914 assert!(!StatuteConflictAnalyzer::is_in_effect(
10915 &statute,
10916 NaiveDate::from_ymd_opt(2019, 1, 1).unwrap()
10917 ));
10918 assert!(!StatuteConflictAnalyzer::is_in_effect(
10919 &statute,
10920 NaiveDate::from_ymd_opt(2031, 1, 1).unwrap()
10921 ));
10922 }
10923
10924 #[test]
10925 fn test_conflict_resolution_resolve_conflicts_at_date() {
10926 let old_general = Statute::new("old-gen", "Old General", Effect::grant("Grant"))
10927 .with_temporal_validity(
10928 TemporalValidity::new()
10929 .with_effective_date(NaiveDate::from_ymd_opt(2015, 1, 1).unwrap()),
10930 );
10931
10932 let new_specific = Statute::new("new-spec", "New Specific", Effect::grant("Grant"))
10933 .with_temporal_validity(
10934 TemporalValidity::new()
10935 .with_effective_date(NaiveDate::from_ymd_opt(2020, 1, 1).unwrap()),
10936 )
10937 .with_precondition(Condition::age(ComparisonOp::GreaterOrEqual, 18));
10938
10939 let federal = Statute::new("federal", "Federal", Effect::grant("Grant"))
10940 .with_temporal_validity(
10941 TemporalValidity::new()
10942 .with_effective_date(NaiveDate::from_ymd_opt(2018, 1, 1).unwrap()),
10943 )
10944 .with_jurisdiction("US");
10945
10946 let expired = Statute::new("expired", "Expired", Effect::grant("Grant"))
10947 .with_temporal_validity(
10948 TemporalValidity::new()
10949 .with_effective_date(NaiveDate::from_ymd_opt(2010, 1, 1).unwrap())
10950 .with_expiry_date(NaiveDate::from_ymd_opt(2015, 12, 31).unwrap()),
10951 );
10952
10953 let statutes = vec![old_general, new_specific, federal, expired];
10954 let active = StatuteConflictAnalyzer::resolve_conflicts_at_date(
10955 &statutes,
10956 NaiveDate::from_ymd_opt(2022, 1, 1).unwrap(),
10957 );
10958
10959 assert_eq!(active.len(), 3);
10961
10962 assert_eq!(active[0].id, "new-spec");
10964 }
10965
10966 #[test]
10967 fn test_conflict_reason_display() {
10968 assert_eq!(
10969 format!("{}", ConflictReason::TemporalPrecedence),
10970 "lex posterior (later law prevails)"
10971 );
10972 assert_eq!(
10973 format!("{}", ConflictReason::Specificity),
10974 "lex specialis (more specific law prevails)"
10975 );
10976 assert_eq!(
10977 format!("{}", ConflictReason::Hierarchy),
10978 "lex superior (higher authority prevails)"
10979 );
10980 }
10981
10982 #[test]
10983 fn test_jurisdiction_level_detection() {
10984 assert_eq!(
10986 StatuteConflictAnalyzer::jurisdiction_level(&Some("US".to_string())),
10987 3
10988 );
10989 assert_eq!(
10990 StatuteConflictAnalyzer::jurisdiction_level(&Some("Federal".to_string())),
10991 3
10992 );
10993
10994 assert_eq!(
10996 StatuteConflictAnalyzer::jurisdiction_level(&Some("US-NY".to_string())),
10997 2
10998 );
10999 assert_eq!(
11000 StatuteConflictAnalyzer::jurisdiction_level(&Some("State-CA".to_string())),
11001 2
11002 );
11003
11004 assert_eq!(
11006 StatuteConflictAnalyzer::jurisdiction_level(&Some("Local-NYC".to_string())),
11007 1
11008 );
11009
11010 assert_eq!(StatuteConflictAnalyzer::jurisdiction_level(&None), 0);
11012 }
11013}