1use serde::{Deserialize, Deserializer, Serialize};
9use std::collections::BTreeSet;
10use std::fmt;
11use thiserror::Error;
12
13const MAX_CONFIDENCE_BASIS_POINTS: u16 = 10_000;
14const MAX_PROBABILITY_PARTS_PER_MILLION: u32 = 1_000_000;
15
16pub const MAX_DERIVATION_PARENTS: usize = 32;
21
22pub const MAX_DERIVATION_ALGORITHM_BYTES: usize = 64;
24
25#[derive(Debug, Clone, PartialEq, Eq, Error)]
27#[non_exhaustive]
28pub enum ReasoningModelError {
29 #[error("{field} must not be empty")]
31 EmptyValue { field: &'static str },
32
33 #[error("confidence score {0} exceeds 10,000 basis points")]
35 ConfidenceOutOfRange(u16),
36
37 #[error("probability {0} exceeds 1,000,000 parts per million")]
39 ProbabilityOutOfRange(u32),
40
41 #[error("Bayesian posterior is undefined for the supplied likelihoods")]
43 UndefinedPosterior,
44
45 #[error("derived evidence must reference at least one parent evidence record")]
47 EmptyDerivationParents,
48
49 #[error("derived evidence references {count} parents, exceeding the maximum of {max}")]
51 TooManyDerivationParents {
52 count: usize,
54 max: usize,
56 },
57
58 #[error("derivation algorithm identifier is {len} bytes, exceeding the maximum of {max}")]
60 DerivationAlgorithmTooLong {
61 len: usize,
63 max: usize,
65 },
66}
67
68fn non_empty(value: impl Into<String>, field: &'static str) -> Result<String, ReasoningModelError> {
69 let value = value.into();
70 if value.trim().is_empty() {
71 return Err(ReasoningModelError::EmptyValue { field });
72 }
73 Ok(value)
74}
75
76fn deserialize_non_empty_string<'de, D>(deserializer: D) -> Result<String, D::Error>
77where
78 D: Deserializer<'de>,
79{
80 let value = String::deserialize(deserializer)?;
81 non_empty(value, "value").map_err(serde::de::Error::custom)
82}
83
84fn deserialize_optional_non_empty_string<'de, D>(
85 deserializer: D,
86) -> Result<Option<String>, D::Error>
87where
88 D: Deserializer<'de>,
89{
90 Option::<String>::deserialize(deserializer)?
91 .map(|value| non_empty(value, "optional value").map_err(serde::de::Error::custom))
92 .transpose()
93}
94
95fn deserialize_non_empty_evidence_ids<'de, D>(
96 deserializer: D,
97) -> Result<BTreeSet<EvidenceId>, D::Error>
98where
99 D: Deserializer<'de>,
100{
101 let evidence_ids = BTreeSet::<EvidenceId>::deserialize(deserializer)?;
102 if evidence_ids.is_empty() {
103 return Err(serde::de::Error::custom(
104 "evidence_ids must contain at least one evidence id",
105 ));
106 }
107 Ok(evidence_ids)
108}
109
110#[derive(Debug, Clone, PartialEq, Eq, PartialOrd, Ord, Hash, Serialize)]
112#[serde(transparent)]
113pub struct EntityId(String);
114
115impl EntityId {
116 pub fn new(value: impl Into<String>) -> Result<Self, ReasoningModelError> {
118 Ok(Self(non_empty(value, "entity id")?))
119 }
120
121 pub fn as_str(&self) -> &str {
123 &self.0
124 }
125}
126
127impl fmt::Display for EntityId {
128 fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
129 formatter.write_str(&self.0)
130 }
131}
132
133impl<'de> Deserialize<'de> for EntityId {
134 fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
135 where
136 D: Deserializer<'de>,
137 {
138 let value = String::deserialize(deserializer)?;
139 Self::new(value).map_err(serde::de::Error::custom)
140 }
141}
142
143#[derive(Debug, Clone, PartialEq, Eq, PartialOrd, Ord, Hash, Serialize)]
145#[serde(transparent)]
146pub struct EvidenceId(String);
147
148impl EvidenceId {
149 pub fn new() -> Self {
151 Self(uuid::Uuid::new_v4().to_string())
152 }
153
154 pub fn parse(value: impl Into<String>) -> Result<Self, ReasoningModelError> {
156 Ok(Self(non_empty(value, "evidence id")?))
157 }
158
159 pub fn as_str(&self) -> &str {
161 &self.0
162 }
163}
164
165impl Default for EvidenceId {
166 fn default() -> Self {
167 Self::new()
168 }
169}
170
171impl fmt::Display for EvidenceId {
172 fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
173 formatter.write_str(&self.0)
174 }
175}
176
177impl<'de> Deserialize<'de> for EvidenceId {
178 fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
179 where
180 D: Deserializer<'de>,
181 {
182 let value = String::deserialize(deserializer)?;
183 Self::parse(value).map_err(serde::de::Error::custom)
184 }
185}
186
187#[derive(Debug, Clone, PartialEq, Eq, PartialOrd, Ord, Hash, Serialize)]
189#[serde(transparent)]
190pub struct RelationId(String);
191
192impl RelationId {
193 pub fn new() -> Self {
195 Self(uuid::Uuid::new_v4().to_string())
196 }
197
198 pub fn parse(value: impl Into<String>) -> Result<Self, ReasoningModelError> {
200 Ok(Self(non_empty(value, "relation id")?))
201 }
202
203 pub fn as_str(&self) -> &str {
205 &self.0
206 }
207}
208
209impl Default for RelationId {
210 fn default() -> Self {
211 Self::new()
212 }
213}
214
215impl fmt::Display for RelationId {
216 fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
217 formatter.write_str(&self.0)
218 }
219}
220
221impl<'de> Deserialize<'de> for RelationId {
222 fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
223 where
224 D: Deserializer<'de>,
225 {
226 let value = String::deserialize(deserializer)?;
227 Self::parse(value).map_err(serde::de::Error::custom)
228 }
229}
230
231#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Hash, Serialize)]
237#[serde(transparent)]
238pub struct ConfidenceScore(u16);
239
240impl ConfidenceScore {
241 pub const NONE: Self = Self(0);
243
244 pub const MAX: Self = Self(MAX_CONFIDENCE_BASIS_POINTS);
246
247 pub fn from_basis_points(value: u16) -> Result<Self, ReasoningModelError> {
249 if value > MAX_CONFIDENCE_BASIS_POINTS {
250 return Err(ReasoningModelError::ConfidenceOutOfRange(value));
251 }
252 Ok(Self(value))
253 }
254
255 pub fn from_percent(value: u8) -> Result<Self, ReasoningModelError> {
257 Self::from_basis_points(u16::from(value) * 100)
258 }
259
260 pub const fn basis_points(self) -> u16 {
262 self.0
263 }
264
265 pub fn ratio(self) -> f64 {
267 f64::from(self.0) / f64::from(MAX_CONFIDENCE_BASIS_POINTS)
268 }
269}
270
271impl<'de> Deserialize<'de> for ConfidenceScore {
272 fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
273 where
274 D: Deserializer<'de>,
275 {
276 let value = u16::deserialize(deserializer)?;
277 Self::from_basis_points(value).map_err(serde::de::Error::custom)
278 }
279}
280
281#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Hash, Serialize)]
290#[serde(transparent)]
291pub struct Probability(u32);
292
293impl Probability {
294 pub const ZERO: Self = Self(0);
296
297 pub const ONE: Self = Self(MAX_PROBABILITY_PARTS_PER_MILLION);
299
300 pub fn from_parts_per_million(value: u32) -> Result<Self, ReasoningModelError> {
302 if value > MAX_PROBABILITY_PARTS_PER_MILLION {
303 return Err(ReasoningModelError::ProbabilityOutOfRange(value));
304 }
305 Ok(Self(value))
306 }
307
308 pub fn from_basis_points(value: u16) -> Result<Self, ReasoningModelError> {
310 Self::from_parts_per_million(u32::from(value) * 100)
311 }
312
313 pub fn from_percent(value: u8) -> Result<Self, ReasoningModelError> {
315 Self::from_parts_per_million(u32::from(value) * 10_000)
316 }
317
318 pub const fn parts_per_million(self) -> u32 {
320 self.0
321 }
322
323 pub fn ratio(self) -> f64 {
325 f64::from(self.0) / f64::from(MAX_PROBABILITY_PARTS_PER_MILLION)
326 }
327
328 pub fn update(
333 self,
334 likelihood_if_true: Self,
335 likelihood_if_false: Self,
336 ) -> Result<Self, ReasoningModelError> {
337 let scale = u128::from(MAX_PROBABILITY_PARTS_PER_MILLION);
338 let prior = u128::from(self.0);
339 let true_weight = u128::from(likelihood_if_true.0) * prior;
340 let false_weight = u128::from(likelihood_if_false.0) * (scale - prior);
341 let denominator = true_weight + false_weight;
342
343 if denominator == 0 {
344 return Err(ReasoningModelError::UndefinedPosterior);
345 }
346
347 let rounded = (true_weight * scale + denominator / 2) / denominator;
348 let value = u32::try_from(rounded).expect("posterior is bounded by the probability scale");
349 Self::from_parts_per_million(value)
350 }
351}
352
353impl<'de> Deserialize<'de> for Probability {
354 fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
355 where
356 D: Deserializer<'de>,
357 {
358 let value = u32::deserialize(deserializer)?;
359 Self::from_parts_per_million(value).map_err(serde::de::Error::custom)
360 }
361}
362
363#[derive(Debug, Clone, PartialEq, Eq, PartialOrd, Ord, Hash, Serialize)]
368pub struct KnowledgePredicate {
369 namespace: String,
370 name: String,
371}
372
373impl KnowledgePredicate {
374 pub fn new(
376 namespace: impl Into<String>,
377 name: impl Into<String>,
378 ) -> Result<Self, ReasoningModelError> {
379 Ok(Self {
380 namespace: non_empty(namespace, "predicate namespace")?,
381 name: non_empty(name, "predicate name")?,
382 })
383 }
384
385 pub fn namespace(&self) -> &str {
387 &self.namespace
388 }
389
390 pub fn name(&self) -> &str {
392 &self.name
393 }
394
395 pub fn dotted(&self) -> String {
397 format!("{}.{}", self.namespace, self.name)
398 }
399}
400
401impl<'de> Deserialize<'de> for KnowledgePredicate {
402 fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
403 where
404 D: Deserializer<'de>,
405 {
406 #[derive(Deserialize)]
407 struct WirePredicate {
408 namespace: String,
409 name: String,
410 }
411
412 let wire = WirePredicate::deserialize(deserializer)?;
413 Self::new(wire.namespace, wire.name).map_err(serde::de::Error::custom)
414 }
415}
416
417#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
419#[serde(tag = "type", content = "value", rename_all = "snake_case")]
420#[non_exhaustive]
421pub enum EvidenceValue {
422 Boolean(bool),
424 Signed(i64),
426 Unsigned(u64),
428 Text(String),
430 TextList(Vec<String>),
432}
433
434#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
436#[serde(rename_all = "snake_case")]
437#[non_exhaustive]
438pub enum EvidenceKind {
439 Network,
441 Http,
443 Tls,
445 Dns,
447 Content,
449 Authentication,
451 RateLimit,
453 Timing,
455 Technology,
457 Custom(String),
459}
460
461#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
463pub struct EvidenceSource {
464 #[serde(deserialize_with = "deserialize_non_empty_string")]
465 component: String,
466 #[serde(deserialize_with = "deserialize_non_empty_string")]
467 method: String,
468 #[serde(default, deserialize_with = "deserialize_optional_non_empty_string")]
469 correlation_id: Option<String>,
470}
471
472impl EvidenceSource {
473 pub fn new(
475 component: impl Into<String>,
476 method: impl Into<String>,
477 ) -> Result<Self, ReasoningModelError> {
478 Ok(Self {
479 component: non_empty(component, "evidence source component")?,
480 method: non_empty(method, "evidence source method")?,
481 correlation_id: None,
482 })
483 }
484
485 pub fn with_correlation_id(
487 mut self,
488 correlation_id: impl Into<String>,
489 ) -> Result<Self, ReasoningModelError> {
490 self.correlation_id = Some(non_empty(correlation_id, "correlation id")?);
491 Ok(self)
492 }
493
494 pub fn component(&self) -> &str {
496 &self.component
497 }
498
499 pub fn method(&self) -> &str {
501 &self.method
502 }
503
504 pub fn correlation_id(&self) -> Option<&str> {
506 self.correlation_id.as_deref()
507 }
508}
509
510#[derive(Debug, Clone, PartialEq, Eq, PartialOrd, Ord, Hash, Serialize)]
518pub struct DerivationAlgorithm {
519 name: String,
520 version: u32,
521}
522
523impl DerivationAlgorithm {
524 pub fn new(name: impl Into<String>, version: u32) -> Result<Self, ReasoningModelError> {
526 let name = non_empty(name, "derivation algorithm name")?;
527 if name.len() > MAX_DERIVATION_ALGORITHM_BYTES {
528 return Err(ReasoningModelError::DerivationAlgorithmTooLong {
529 len: name.len(),
530 max: MAX_DERIVATION_ALGORITHM_BYTES,
531 });
532 }
533 Ok(Self { name, version })
534 }
535
536 pub fn name(&self) -> &str {
538 &self.name
539 }
540
541 pub fn version(&self) -> u32 {
543 self.version
544 }
545}
546
547impl<'de> Deserialize<'de> for DerivationAlgorithm {
548 fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
549 where
550 D: Deserializer<'de>,
551 {
552 #[derive(Deserialize)]
553 #[serde(deny_unknown_fields)]
554 struct Wire {
555 name: String,
556 version: u32,
557 }
558 let wire = Wire::deserialize(deserializer)?;
559 Self::new(wire.name, wire.version).map_err(serde::de::Error::custom)
560 }
561}
562
563#[derive(Debug, Clone, PartialEq, Eq, Serialize)]
573pub struct EvidenceDerivation {
574 parents: Vec<EvidenceId>,
575 algorithm: DerivationAlgorithm,
576}
577
578impl EvidenceDerivation {
579 pub fn new(
585 parents: impl IntoIterator<Item = EvidenceId>,
586 algorithm: DerivationAlgorithm,
587 ) -> Result<Self, ReasoningModelError> {
588 let canonical: BTreeSet<EvidenceId> = parents.into_iter().collect();
589 if canonical.is_empty() {
590 return Err(ReasoningModelError::EmptyDerivationParents);
591 }
592 if canonical.len() > MAX_DERIVATION_PARENTS {
593 return Err(ReasoningModelError::TooManyDerivationParents {
594 count: canonical.len(),
595 max: MAX_DERIVATION_PARENTS,
596 });
597 }
598 Ok(Self {
599 parents: canonical.into_iter().collect(),
600 algorithm,
601 })
602 }
603
604 pub fn parents(&self) -> &[EvidenceId] {
606 &self.parents
607 }
608
609 pub fn algorithm(&self) -> &DerivationAlgorithm {
611 &self.algorithm
612 }
613
614 pub fn references_parent(&self, id: &EvidenceId) -> bool {
616 self.parents.binary_search(id).is_ok()
617 }
618}
619
620impl<'de> Deserialize<'de> for EvidenceDerivation {
621 fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
622 where
623 D: Deserializer<'de>,
624 {
625 #[derive(Deserialize)]
626 #[serde(deny_unknown_fields)]
627 struct Wire {
628 parents: Vec<EvidenceId>,
629 algorithm: DerivationAlgorithm,
630 }
631 let wire = Wire::deserialize(deserializer)?;
632 Self::new(wire.parents, wire.algorithm).map_err(serde::de::Error::custom)
633 }
634}
635
636#[derive(Debug, Clone, Default, PartialEq, Eq, Serialize, Deserialize)]
644#[serde(rename_all = "snake_case")]
645#[non_exhaustive]
646pub enum EvidenceOrigin {
647 #[default]
649 Direct,
650 Derived(EvidenceDerivation),
652}
653
654impl EvidenceOrigin {
655 pub fn is_direct(&self) -> bool {
657 matches!(self, Self::Direct)
658 }
659
660 pub fn derivation(&self) -> Option<&EvidenceDerivation> {
662 match self {
663 Self::Derived(derivation) => Some(derivation),
664 Self::Direct => None,
665 }
666 }
667}
668
669#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
704pub struct Evidence {
705 id: EvidenceId,
706 subject: EntityId,
707 kind: EvidenceKind,
708 predicate: KnowledgePredicate,
709 value: EvidenceValue,
710 source: EvidenceSource,
711 reliability: ConfidenceScore,
712 observed_at_ms: u64,
713 #[serde(skip)]
719 origin: EvidenceOrigin,
720}
721
722impl Evidence {
723 pub fn new(
725 subject: EntityId,
726 kind: EvidenceKind,
727 predicate: KnowledgePredicate,
728 value: EvidenceValue,
729 source: EvidenceSource,
730 reliability: ConfidenceScore,
731 ) -> Self {
732 Self::with_id(
733 EvidenceId::new(),
734 subject,
735 kind,
736 predicate,
737 value,
738 source,
739 reliability,
740 )
741 }
742
743 pub fn with_id(
749 id: EvidenceId,
750 subject: EntityId,
751 kind: EvidenceKind,
752 predicate: KnowledgePredicate,
753 value: EvidenceValue,
754 source: EvidenceSource,
755 reliability: ConfidenceScore,
756 ) -> Self {
757 Self::with_id_at(
758 id,
759 subject,
760 kind,
761 predicate,
762 value,
763 source,
764 reliability,
765 now_ms(),
766 )
767 }
768
769 #[allow(clippy::too_many_arguments)]
774 pub fn with_id_at(
775 id: EvidenceId,
776 subject: EntityId,
777 kind: EvidenceKind,
778 predicate: KnowledgePredicate,
779 value: EvidenceValue,
780 source: EvidenceSource,
781 reliability: ConfidenceScore,
782 observed_at_ms: u64,
783 ) -> Self {
784 Self {
785 id,
786 subject,
787 kind,
788 predicate,
789 value,
790 source,
791 reliability,
792 observed_at_ms,
793 origin: EvidenceOrigin::Direct,
794 }
795 }
796
797 pub fn derived_from(mut self, derivation: EvidenceDerivation) -> Self {
804 self.origin = EvidenceOrigin::Derived(derivation);
805 self
806 }
807
808 pub fn id(&self) -> &EvidenceId {
810 &self.id
811 }
812
813 pub fn subject(&self) -> &EntityId {
815 &self.subject
816 }
817
818 pub fn kind(&self) -> &EvidenceKind {
820 &self.kind
821 }
822
823 pub fn predicate(&self) -> &KnowledgePredicate {
825 &self.predicate
826 }
827
828 pub fn value(&self) -> &EvidenceValue {
830 &self.value
831 }
832
833 pub fn source(&self) -> &EvidenceSource {
835 &self.source
836 }
837
838 pub fn reliability(&self) -> ConfidenceScore {
840 self.reliability
841 }
842
843 pub fn observed_at_ms(&self) -> u64 {
848 self.observed_at_ms
849 }
850
851 pub fn origin(&self) -> &EvidenceOrigin {
853 &self.origin
854 }
855}
856
857#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
859pub struct Fact {
860 #[serde(deserialize_with = "deserialize_non_empty_string")]
861 id: String,
862 subject: EntityId,
863 predicate: KnowledgePredicate,
864 value: EvidenceValue,
865 confidence: ConfidenceScore,
866 #[serde(deserialize_with = "deserialize_non_empty_evidence_ids")]
867 evidence_ids: BTreeSet<EvidenceId>,
868 asserted_at_ms: u64,
869}
870
871impl Fact {
872 pub fn new(
874 subject: EntityId,
875 predicate: KnowledgePredicate,
876 value: EvidenceValue,
877 confidence: ConfidenceScore,
878 evidence_id: EvidenceId,
879 ) -> Self {
880 Self {
881 id: uuid::Uuid::new_v4().to_string(),
882 subject,
883 predicate,
884 value,
885 confidence,
886 evidence_ids: BTreeSet::from([evidence_id]),
887 asserted_at_ms: now_ms(),
888 }
889 }
890
891 pub fn with_confidence(mut self, confidence: ConfidenceScore) -> Self {
893 self.confidence = confidence;
894 self
895 }
896
897 pub fn add_evidence(&mut self, evidence_id: EvidenceId) {
899 self.evidence_ids.insert(evidence_id);
900 }
901
902 pub fn id(&self) -> &str {
904 &self.id
905 }
906
907 pub fn subject(&self) -> &EntityId {
909 &self.subject
910 }
911
912 pub fn predicate(&self) -> &KnowledgePredicate {
914 &self.predicate
915 }
916
917 pub fn value(&self) -> &EvidenceValue {
919 &self.value
920 }
921
922 pub fn confidence(&self) -> ConfidenceScore {
924 self.confidence
925 }
926
927 pub fn evidence_ids(&self) -> &BTreeSet<EvidenceId> {
929 &self.evidence_ids
930 }
931
932 pub fn asserted_at_ms(&self) -> u64 {
934 self.asserted_at_ms
935 }
936}
937
938#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
940#[serde(rename_all = "snake_case")]
941#[non_exhaustive]
942pub enum ContributionDirection {
943 Supporting,
945 Contradicting,
947}
948
949#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
951pub struct EvidenceContribution {
952 evidence_id: EvidenceId,
953 direction: ContributionDirection,
954 weight: ConfidenceScore,
955 #[serde(deserialize_with = "deserialize_non_empty_string")]
956 rationale: String,
957}
958
959impl EvidenceContribution {
960 pub fn new(
962 evidence_id: EvidenceId,
963 direction: ContributionDirection,
964 weight: ConfidenceScore,
965 rationale: impl Into<String>,
966 ) -> Result<Self, ReasoningModelError> {
967 Ok(Self {
968 evidence_id,
969 direction,
970 weight,
971 rationale: non_empty(rationale, "evidence contribution rationale")?,
972 })
973 }
974
975 pub fn evidence_id(&self) -> &EvidenceId {
977 &self.evidence_id
978 }
979
980 pub fn direction(&self) -> ContributionDirection {
982 self.direction
983 }
984
985 pub fn weight(&self) -> ConfidenceScore {
987 self.weight
988 }
989
990 pub fn rationale(&self) -> &str {
992 &self.rationale
993 }
994}
995
996#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
1014pub struct BayesianEvidence {
1015 evidence_id: EvidenceId,
1016 likelihood_if_true: Probability,
1017 likelihood_if_false: Probability,
1018 #[serde(deserialize_with = "deserialize_non_empty_string")]
1019 rationale: String,
1020}
1021
1022impl BayesianEvidence {
1023 pub fn new(
1025 evidence_id: EvidenceId,
1026 likelihood_if_true: Probability,
1027 likelihood_if_false: Probability,
1028 rationale: impl Into<String>,
1029 ) -> Result<Self, ReasoningModelError> {
1030 Ok(Self {
1031 evidence_id,
1032 likelihood_if_true,
1033 likelihood_if_false,
1034 rationale: non_empty(rationale, "Bayesian evidence rationale")?,
1035 })
1036 }
1037
1038 pub fn evidence_id(&self) -> &EvidenceId {
1040 &self.evidence_id
1041 }
1042
1043 pub fn likelihood_if_true(&self) -> Probability {
1045 self.likelihood_if_true
1046 }
1047
1048 pub fn likelihood_if_false(&self) -> Probability {
1050 self.likelihood_if_false
1051 }
1052
1053 pub fn rationale(&self) -> &str {
1055 &self.rationale
1056 }
1057}
1058
1059#[derive(Debug, Clone, Copy, PartialEq, Eq)]
1061#[non_exhaustive]
1062pub enum BeliefWrite {
1063 Inserted,
1065 Replaced,
1067 Unchanged,
1069}
1070
1071#[derive(Debug, Clone, PartialEq, Eq)]
1073pub struct BayesianUpdate {
1074 evidence_id: EvidenceId,
1075 prior: Probability,
1076 likelihood_if_true: Probability,
1077 likelihood_if_false: Probability,
1078 posterior: Probability,
1079 rationale: String,
1080}
1081
1082impl BayesianUpdate {
1083 pub fn evidence_id(&self) -> &EvidenceId {
1085 &self.evidence_id
1086 }
1087
1088 pub fn prior(&self) -> Probability {
1090 self.prior
1091 }
1092
1093 pub fn likelihood_if_true(&self) -> Probability {
1095 self.likelihood_if_true
1096 }
1097
1098 pub fn likelihood_if_false(&self) -> Probability {
1100 self.likelihood_if_false
1101 }
1102
1103 pub fn posterior(&self) -> Probability {
1105 self.posterior
1106 }
1107
1108 pub fn rationale(&self) -> &str {
1110 &self.rationale
1111 }
1112}
1113
1114#[derive(Debug, Clone, PartialEq, Eq, Serialize)]
1121pub struct BayesianBelief {
1122 prior: Probability,
1123 posterior: Probability,
1124 evidence: Vec<BayesianEvidence>,
1125 #[serde(skip)]
1126 updates: Vec<BayesianUpdate>,
1127}
1128
1129impl BayesianBelief {
1130 pub fn new(prior: Probability) -> Self {
1132 Self {
1133 prior,
1134 posterior: prior,
1135 evidence: Vec::new(),
1136 updates: Vec::new(),
1137 }
1138 }
1139
1140 pub fn observe(
1145 &mut self,
1146 observation: BayesianEvidence,
1147 ) -> Result<BeliefWrite, ReasoningModelError> {
1148 let mut candidate = self.evidence.clone();
1149 let write = match candidate
1150 .binary_search_by(|existing| existing.evidence_id.cmp(&observation.evidence_id))
1151 {
1152 Ok(index) if candidate[index] == observation => return Ok(BeliefWrite::Unchanged),
1153 Ok(index) => {
1154 candidate[index] = observation;
1155 BeliefWrite::Replaced
1156 },
1157 Err(index) => {
1158 candidate.insert(index, observation);
1159 BeliefWrite::Inserted
1160 },
1161 };
1162 let (posterior, updates) = calculate_bayesian_updates(self.prior, &candidate)?;
1163
1164 self.posterior = posterior;
1165 self.evidence = candidate;
1166 self.updates = updates;
1167 Ok(write)
1168 }
1169
1170 pub fn prior(&self) -> Probability {
1172 self.prior
1173 }
1174
1175 pub fn posterior(&self) -> Probability {
1177 self.posterior
1178 }
1179
1180 pub fn evidence(&self) -> &[BayesianEvidence] {
1182 &self.evidence
1183 }
1184
1185 pub fn updates(&self) -> &[BayesianUpdate] {
1187 &self.updates
1188 }
1189}
1190
1191impl<'de> Deserialize<'de> for BayesianBelief {
1192 fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
1193 where
1194 D: Deserializer<'de>,
1195 {
1196 #[derive(Deserialize)]
1197 struct WireBelief {
1198 prior: Probability,
1199 posterior: Probability,
1200 evidence: Vec<BayesianEvidence>,
1201 }
1202
1203 let wire = WireBelief::deserialize(deserializer)?;
1204 let mut belief = Self::new(wire.prior);
1205 let mut evidence_ids = BTreeSet::new();
1206 for observation in wire.evidence {
1207 if !evidence_ids.insert(observation.evidence_id.clone()) {
1208 return Err(serde::de::Error::custom(format!(
1209 "duplicate Bayesian evidence id {}",
1210 observation.evidence_id
1211 )));
1212 }
1213 belief
1214 .observe(observation)
1215 .map_err(serde::de::Error::custom)?;
1216 }
1217 if belief.posterior != wire.posterior {
1218 return Err(serde::de::Error::custom(format!(
1219 "serialized posterior {} does not match computed posterior {}",
1220 wire.posterior.parts_per_million(),
1221 belief.posterior.parts_per_million()
1222 )));
1223 }
1224 Ok(belief)
1225 }
1226}
1227
1228fn calculate_bayesian_updates(
1229 prior: Probability,
1230 evidence: &[BayesianEvidence],
1231) -> Result<(Probability, Vec<BayesianUpdate>), ReasoningModelError> {
1232 let mut current = prior;
1233 let mut updates = Vec::with_capacity(evidence.len());
1234 for observation in evidence {
1235 let posterior = current.update(
1236 observation.likelihood_if_true,
1237 observation.likelihood_if_false,
1238 )?;
1239 updates.push(BayesianUpdate {
1240 evidence_id: observation.evidence_id.clone(),
1241 prior: current,
1242 likelihood_if_true: observation.likelihood_if_true,
1243 likelihood_if_false: observation.likelihood_if_false,
1244 posterior,
1245 rationale: observation.rationale.clone(),
1246 });
1247 current = posterior;
1248 }
1249 Ok((current, updates))
1250}
1251
1252#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
1254#[serde(rename_all = "snake_case")]
1255#[non_exhaustive]
1256pub enum HypothesisState {
1257 Proposed,
1259 Supported,
1261 Contradicted,
1263 Confirmed,
1265 Rejected,
1267}
1268
1269#[derive(Debug, Clone, Copy, Default, PartialEq, Eq, Serialize, Deserialize)]
1275#[serde(rename_all = "snake_case")]
1276#[non_exhaustive]
1277pub enum HypothesisStrength {
1278 #[default]
1280 Weak,
1281 Strong,
1283}
1284
1285#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
1287pub struct Hypothesis {
1288 #[serde(deserialize_with = "deserialize_non_empty_string")]
1289 id: String,
1290 subject: EntityId,
1291 predicate: KnowledgePredicate,
1292 value: EvidenceValue,
1293 belief: BayesianBelief,
1294 strength: HypothesisStrength,
1295 state: HypothesisState,
1296 updated_at_ms: u64,
1297}
1298
1299impl Hypothesis {
1300 pub fn new(
1302 subject: EntityId,
1303 predicate: KnowledgePredicate,
1304 value: EvidenceValue,
1305 prior: Probability,
1306 ) -> Self {
1307 Self::with_id(
1308 uuid::Uuid::new_v4().to_string(),
1309 subject,
1310 predicate,
1311 value,
1312 prior,
1313 )
1314 .expect("generated UUID is a non-empty hypothesis identifier")
1315 }
1316
1317 pub fn with_id(
1322 id: impl Into<String>,
1323 subject: EntityId,
1324 predicate: KnowledgePredicate,
1325 value: EvidenceValue,
1326 prior: Probability,
1327 ) -> Result<Self, ReasoningModelError> {
1328 Ok(Self {
1329 id: non_empty(id, "hypothesis id")?,
1330 subject,
1331 predicate,
1332 value,
1333 belief: BayesianBelief::new(prior),
1334 strength: HypothesisStrength::Weak,
1335 state: HypothesisState::Proposed,
1336 updated_at_ms: now_ms(),
1337 })
1338 }
1339
1340 pub fn observe(
1342 &mut self,
1343 observation: BayesianEvidence,
1344 ) -> Result<BeliefWrite, ReasoningModelError> {
1345 let write = self.belief.observe(observation)?;
1346 if write != BeliefWrite::Unchanged {
1347 self.updated_at_ms = now_ms();
1348 }
1349 Ok(write)
1350 }
1351
1352 pub fn set_strength(&mut self, strength: HypothesisStrength) {
1354 self.strength = strength;
1355 self.updated_at_ms = now_ms();
1356 }
1357
1358 pub fn set_state(&mut self, state: HypothesisState) {
1360 self.state = state;
1361 self.updated_at_ms = now_ms();
1362 }
1363
1364 pub fn id(&self) -> &str {
1366 &self.id
1367 }
1368
1369 pub fn subject(&self) -> &EntityId {
1371 &self.subject
1372 }
1373
1374 pub fn predicate(&self) -> &KnowledgePredicate {
1376 &self.predicate
1377 }
1378
1379 pub fn value(&self) -> &EvidenceValue {
1381 &self.value
1382 }
1383
1384 pub fn belief(&self) -> &BayesianBelief {
1386 &self.belief
1387 }
1388
1389 pub fn prior(&self) -> Probability {
1391 self.belief.prior()
1392 }
1393
1394 pub fn posterior(&self) -> Probability {
1396 self.belief.posterior()
1397 }
1398
1399 pub fn strength(&self) -> HypothesisStrength {
1401 self.strength
1402 }
1403
1404 pub fn state(&self) -> HypothesisState {
1406 self.state
1407 }
1408
1409 pub fn updated_at_ms(&self) -> u64 {
1411 self.updated_at_ms
1412 }
1413
1414 pub fn same_evaluation_as(&self, other: &Self) -> bool {
1419 self.id == other.id
1420 && self.subject == other.subject
1421 && self.predicate == other.predicate
1422 && self.value == other.value
1423 && self.belief == other.belief
1424 && self.strength == other.strength
1425 && self.state == other.state
1426 }
1427}
1428
1429#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
1431#[serde(rename_all = "snake_case")]
1432#[non_exhaustive]
1433pub enum EntityKind {
1434 Host,
1436 Service,
1438 Endpoint,
1440 Technology,
1442 Identity,
1444 Session,
1446 Parameter,
1448 Credential,
1450 Custom(String),
1452}
1453
1454#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
1456pub struct KnowledgeEntity {
1457 id: EntityId,
1458 kind: EntityKind,
1459 #[serde(deserialize_with = "deserialize_non_empty_string")]
1460 label: String,
1461}
1462
1463impl KnowledgeEntity {
1464 pub fn new(
1466 id: EntityId,
1467 kind: EntityKind,
1468 label: impl Into<String>,
1469 ) -> Result<Self, ReasoningModelError> {
1470 Ok(Self {
1471 id,
1472 kind,
1473 label: non_empty(label, "entity label")?,
1474 })
1475 }
1476
1477 pub fn id(&self) -> &EntityId {
1479 &self.id
1480 }
1481
1482 pub fn kind(&self) -> &EntityKind {
1484 &self.kind
1485 }
1486
1487 pub fn label(&self) -> &str {
1489 &self.label
1490 }
1491}
1492
1493#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
1495#[serde(rename_all = "snake_case")]
1496#[non_exhaustive]
1497pub enum RelationKind {
1498 Exposes,
1500 Serves,
1502 Uses,
1504 DependsOn,
1506 Contains,
1508 AuthenticatesWith,
1510 DerivedFrom,
1512 RelatedTo,
1514 Custom(String),
1516}
1517
1518#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
1520pub struct KnowledgeRelation {
1521 id: RelationId,
1522 from: EntityId,
1523 to: EntityId,
1524 kind: RelationKind,
1525 confidence: ConfidenceScore,
1526 #[serde(deserialize_with = "deserialize_non_empty_evidence_ids")]
1527 evidence_ids: BTreeSet<EvidenceId>,
1528}
1529
1530impl KnowledgeRelation {
1531 pub fn new(
1533 from: EntityId,
1534 to: EntityId,
1535 kind: RelationKind,
1536 confidence: ConfidenceScore,
1537 evidence_id: EvidenceId,
1538 ) -> Self {
1539 Self::with_id(RelationId::new(), from, to, kind, confidence, evidence_id)
1540 }
1541
1542 pub fn with_id(
1544 id: RelationId,
1545 from: EntityId,
1546 to: EntityId,
1547 kind: RelationKind,
1548 confidence: ConfidenceScore,
1549 evidence_id: EvidenceId,
1550 ) -> Self {
1551 Self {
1552 id,
1553 from,
1554 to,
1555 kind,
1556 confidence,
1557 evidence_ids: BTreeSet::from([evidence_id]),
1558 }
1559 }
1560
1561 pub fn add_evidence(&mut self, evidence_id: EvidenceId) {
1563 self.evidence_ids.insert(evidence_id);
1564 }
1565
1566 pub fn id(&self) -> &RelationId {
1568 &self.id
1569 }
1570
1571 pub fn from(&self) -> &EntityId {
1573 &self.from
1574 }
1575
1576 pub fn to(&self) -> &EntityId {
1578 &self.to
1579 }
1580
1581 pub fn kind(&self) -> &RelationKind {
1583 &self.kind
1584 }
1585
1586 pub fn confidence(&self) -> ConfidenceScore {
1588 self.confidence
1589 }
1590
1591 pub fn evidence_ids(&self) -> &BTreeSet<EvidenceId> {
1593 &self.evidence_ids
1594 }
1595}
1596
1597fn now_ms() -> u64 {
1598 let milliseconds = std::time::SystemTime::now()
1599 .duration_since(std::time::UNIX_EPOCH)
1600 .unwrap_or_default()
1601 .as_millis();
1602 u64::try_from(milliseconds).unwrap_or(u64::MAX)
1603}
1604
1605#[cfg(test)]
1606mod tests {
1607 use super::*;
1608
1609 fn subject() -> EntityId {
1610 EntityId::new("endpoint:https://example.test/api").unwrap()
1611 }
1612
1613 fn predicate() -> KnowledgePredicate {
1614 KnowledgePredicate::new("technology", "framework").unwrap()
1615 }
1616
1617 fn source() -> EvidenceSource {
1618 EvidenceSource::new("fingerprint.headers", "x-powered-by")
1619 .unwrap()
1620 .with_correlation_id("scan-42")
1621 .unwrap()
1622 }
1623
1624 fn evidence() -> Evidence {
1625 Evidence::new(
1626 subject(),
1627 EvidenceKind::Technology,
1628 predicate(),
1629 EvidenceValue::Text("Laravel".into()),
1630 source(),
1631 ConfidenceScore::from_percent(90).unwrap(),
1632 )
1633 }
1634
1635 #[test]
1636 fn confidence_rejects_out_of_range_values() {
1637 assert_eq!(
1638 ConfidenceScore::from_basis_points(10_001),
1639 Err(ReasoningModelError::ConfidenceOutOfRange(10_001))
1640 );
1641 assert_eq!(
1642 ConfidenceScore::from_percent(101),
1643 Err(ReasoningModelError::ConfidenceOutOfRange(10_100))
1644 );
1645 }
1646
1647 #[test]
1648 fn confidence_rejects_invalid_wire_values() {
1649 assert!(serde_json::from_str::<ConfidenceScore>("10001").is_err());
1650 assert_eq!(
1651 serde_json::from_str::<ConfidenceScore>("8200").unwrap(),
1652 ConfidenceScore::from_percent(82).unwrap()
1653 );
1654 }
1655
1656 #[test]
1657 fn probability_rejects_out_of_range_values() {
1658 assert_eq!(
1659 Probability::from_parts_per_million(1_000_001),
1660 Err(ReasoningModelError::ProbabilityOutOfRange(1_000_001))
1661 );
1662 assert_eq!(
1663 Probability::from_percent(101),
1664 Err(ReasoningModelError::ProbabilityOutOfRange(1_010_000))
1665 );
1666 assert!(serde_json::from_str::<Probability>("1000001").is_err());
1667 }
1668
1669 #[test]
1670 fn probability_applies_bayes_theorem_with_fixed_point_rounding() {
1671 let posterior = Probability::from_percent(10)
1672 .unwrap()
1673 .update(
1674 Probability::from_percent(80).unwrap(),
1675 Probability::from_percent(20).unwrap(),
1676 )
1677 .unwrap();
1678
1679 assert_eq!(posterior.parts_per_million(), 307_692);
1680 assert_eq!(
1681 Probability::ZERO.update(Probability::ZERO, Probability::ZERO),
1682 Err(ReasoningModelError::UndefinedPosterior)
1683 );
1684 }
1685
1686 #[test]
1687 fn evidence_round_trip_preserves_provenance() {
1688 let evidence = evidence();
1689 let encoded = serde_json::to_string(&evidence).unwrap();
1690 let decoded: Evidence = serde_json::from_str(&encoded).unwrap();
1691
1692 assert_eq!(decoded, evidence);
1693 assert_eq!(decoded.source().component(), "fingerprint.headers");
1694 assert_eq!(decoded.source().correlation_id(), Some("scan-42"));
1695 assert_eq!(decoded.reliability().basis_points(), 9_000);
1696 }
1697
1698 fn algorithm() -> DerivationAlgorithm {
1699 DerivationAlgorithm::new("test.transform", 1).unwrap()
1700 }
1701
1702 #[test]
1703 fn derivation_algorithm_validates_name_bounds() {
1704 assert_eq!(
1705 DerivationAlgorithm::new(" ", 1),
1706 Err(ReasoningModelError::EmptyValue {
1707 field: "derivation algorithm name"
1708 })
1709 );
1710 let long = "a".repeat(MAX_DERIVATION_ALGORITHM_BYTES + 1);
1711 assert!(matches!(
1712 DerivationAlgorithm::new(long, 1),
1713 Err(ReasoningModelError::DerivationAlgorithmTooLong { .. })
1714 ));
1715 }
1716
1717 #[test]
1718 fn derivation_rejects_empty_parent_set() {
1719 assert_eq!(
1720 EvidenceDerivation::new(Vec::new(), algorithm()),
1721 Err(ReasoningModelError::EmptyDerivationParents)
1722 );
1723 }
1724
1725 #[test]
1726 fn derivation_canonicalizes_parents_order_independently() {
1727 let a = EvidenceId::parse("id-a").unwrap();
1728 let b = EvidenceId::parse("id-b").unwrap();
1729 let forward =
1730 EvidenceDerivation::new([a.clone(), b.clone(), a.clone()], algorithm()).unwrap();
1731 let reverse = EvidenceDerivation::new([b.clone(), a.clone()], algorithm()).unwrap();
1732
1733 assert_eq!(forward, reverse);
1735 assert_eq!(forward.parents(), &[a.clone(), b.clone()]);
1736 assert!(forward.references_parent(&a));
1737 assert!(!forward.references_parent(&EvidenceId::parse("id-c").unwrap()));
1738 }
1739
1740 #[test]
1741 fn derivation_rejects_more_parents_than_the_bound() {
1742 let parents: Vec<EvidenceId> = (0..=MAX_DERIVATION_PARENTS)
1743 .map(|index| EvidenceId::parse(format!("parent-{index}")).unwrap())
1744 .collect();
1745 assert!(matches!(
1746 EvidenceDerivation::new(parents, algorithm()),
1747 Err(ReasoningModelError::TooManyDerivationParents { .. })
1748 ));
1749 }
1750
1751 #[test]
1752 fn derived_from_sets_lineage_and_participates_in_equality() {
1753 let parent = EvidenceId::parse("body-sample").unwrap();
1754 let derivation = EvidenceDerivation::new([parent.clone()], algorithm()).unwrap();
1755 let direct = evidence();
1756 let derived = evidence().derived_from(derivation.clone());
1757
1758 assert!(direct.origin().is_direct());
1759 assert_eq!(derived.origin().derivation(), Some(&derivation));
1760 assert_eq!(derived.origin().derivation().unwrap().parents(), &[parent]);
1761 let direct_same_id = Evidence::with_id(
1764 derived.id().clone(),
1765 derived.subject().clone(),
1766 derived.kind().clone(),
1767 derived.predicate().clone(),
1768 derived.value().clone(),
1769 derived.source().clone(),
1770 derived.reliability(),
1771 );
1772 assert_ne!(direct_same_id, derived);
1773 }
1774
1775 #[test]
1776 fn derived_evidence_wire_is_byte_identical_to_direct_and_omits_lineage() {
1777 let parent = EvidenceId::parse("body-sample").unwrap();
1782 let derivation = EvidenceDerivation::new([parent], algorithm()).unwrap();
1783 let base = evidence();
1784 let derived = base.clone().derived_from(derivation);
1785
1786 let direct_wire = serde_json::to_value(&base).unwrap();
1787 let derived_wire = serde_json::to_value(&derived).unwrap();
1788 assert_eq!(direct_wire, derived_wire);
1789 assert!(direct_wire.get("origin").is_none());
1790
1791 let restored: Evidence = serde_json::from_value(derived_wire).unwrap();
1792 assert!(restored.origin().is_direct());
1793 }
1794
1795 #[test]
1796 fn fact_deduplicates_evidence_provenance() {
1797 let evidence = evidence();
1798 let evidence_id = evidence.id().clone();
1799 let mut fact = Fact::new(
1800 evidence.subject().clone(),
1801 evidence.predicate().clone(),
1802 evidence.value().clone(),
1803 ConfidenceScore::from_percent(90).unwrap(),
1804 evidence_id.clone(),
1805 );
1806
1807 fact.add_evidence(evidence_id);
1808
1809 assert_eq!(fact.evidence_ids().len(), 1);
1810 assert_eq!(fact.confidence().basis_points(), 9_000);
1811 }
1812
1813 #[test]
1814 fn hypothesis_uses_bayesian_belief_and_explicit_strength() {
1815 let evidence = evidence();
1816 let mut hypothesis = Hypothesis::new(
1817 evidence.subject().clone(),
1818 evidence.predicate().clone(),
1819 evidence.value().clone(),
1820 Probability::from_percent(10).unwrap(),
1821 );
1822 assert_eq!(hypothesis.strength(), HypothesisStrength::Weak);
1823 assert_eq!(
1824 hypothesis
1825 .observe(
1826 BayesianEvidence::new(
1827 evidence.id().clone(),
1828 Probability::from_percent(80).unwrap(),
1829 Probability::from_percent(20).unwrap(),
1830 "framework header observed",
1831 )
1832 .unwrap(),
1833 )
1834 .unwrap(),
1835 BeliefWrite::Inserted
1836 );
1837 assert_eq!(hypothesis.posterior().parts_per_million(), 307_692);
1838 hypothesis.set_strength(HypothesisStrength::Strong);
1839 hypothesis.set_state(HypothesisState::Supported);
1840
1841 assert_eq!(hypothesis.belief().evidence().len(), 1);
1842 assert_eq!(hypothesis.belief().updates().len(), 1);
1843 assert_eq!(hypothesis.strength(), HypothesisStrength::Strong);
1844 assert_eq!(hypothesis.state(), HypothesisState::Supported);
1845 }
1846
1847 #[test]
1848 fn hypothesis_stable_identity_ignores_wall_clock_for_idempotency() {
1849 let evidence = evidence();
1850 let first = Hypothesis::with_id(
1851 "rule:laravel:endpoint",
1852 evidence.subject().clone(),
1853 evidence.predicate().clone(),
1854 evidence.value().clone(),
1855 Probability::from_percent(10).unwrap(),
1856 )
1857 .unwrap();
1858 let second = Hypothesis::with_id(
1859 "rule:laravel:endpoint",
1860 evidence.subject().clone(),
1861 evidence.predicate().clone(),
1862 evidence.value().clone(),
1863 Probability::from_percent(10).unwrap(),
1864 )
1865 .unwrap();
1866
1867 assert!(first.same_evaluation_as(&second));
1868 assert!(Hypothesis::with_id(
1869 " ",
1870 evidence.subject().clone(),
1871 evidence.predicate().clone(),
1872 evidence.value().clone(),
1873 Probability::from_percent(10).unwrap(),
1874 )
1875 .is_err());
1876 }
1877
1878 #[test]
1879 fn bayesian_belief_is_idempotent_and_order_independent() {
1880 let first = BayesianEvidence::new(
1881 EvidenceId::parse("evidence:a").unwrap(),
1882 Probability::from_percent(80).unwrap(),
1883 Probability::from_percent(20).unwrap(),
1884 "specific cookie",
1885 )
1886 .unwrap();
1887 let second = BayesianEvidence::new(
1888 EvidenceId::parse("evidence:b").unwrap(),
1889 Probability::from_percent(70).unwrap(),
1890 Probability::from_percent(30).unwrap(),
1891 "framework header",
1892 )
1893 .unwrap();
1894 let mut forward = BayesianBelief::new(Probability::from_percent(10).unwrap());
1895 let mut reverse = BayesianBelief::new(Probability::from_percent(10).unwrap());
1896
1897 assert_eq!(
1898 forward.observe(first.clone()).unwrap(),
1899 BeliefWrite::Inserted
1900 );
1901 assert_eq!(
1902 forward.observe(second.clone()).unwrap(),
1903 BeliefWrite::Inserted
1904 );
1905 assert_eq!(
1906 forward.observe(first.clone()).unwrap(),
1907 BeliefWrite::Unchanged
1908 );
1909 reverse.observe(second).unwrap();
1910 reverse.observe(first).unwrap();
1911
1912 assert_eq!(forward, reverse);
1913 assert_eq!(forward.evidence().len(), 2);
1914 assert_eq!(forward.updates()[0].evidence_id().as_str(), "evidence:a");
1915 assert_eq!(forward.updates()[1].evidence_id().as_str(), "evidence:b");
1916 }
1917
1918 #[test]
1919 fn bayesian_belief_replaces_likelihoods_transactionally() {
1920 let evidence_id = EvidenceId::parse("evidence:cookie").unwrap();
1921 let mut belief = BayesianBelief::new(Probability::from_percent(10).unwrap());
1922 belief
1923 .observe(
1924 BayesianEvidence::new(
1925 evidence_id.clone(),
1926 Probability::from_percent(80).unwrap(),
1927 Probability::from_percent(20).unwrap(),
1928 "initial calibration",
1929 )
1930 .unwrap(),
1931 )
1932 .unwrap();
1933 let original = belief.clone();
1934
1935 assert_eq!(
1936 belief
1937 .observe(
1938 BayesianEvidence::new(
1939 evidence_id.clone(),
1940 Probability::from_percent(90).unwrap(),
1941 Probability::from_percent(10).unwrap(),
1942 "recalibrated",
1943 )
1944 .unwrap(),
1945 )
1946 .unwrap(),
1947 BeliefWrite::Replaced
1948 );
1949 assert_eq!(belief.evidence().len(), 1);
1950 assert_ne!(belief.posterior(), original.posterior());
1951
1952 let before_invalid_update = belief.clone();
1953 assert_eq!(
1954 belief.observe(
1955 BayesianEvidence::new(
1956 evidence_id,
1957 Probability::ZERO,
1958 Probability::ZERO,
1959 "invalid calibration",
1960 )
1961 .unwrap()
1962 ),
1963 Err(ReasoningModelError::UndefinedPosterior)
1964 );
1965 assert_eq!(belief, before_invalid_update);
1966 }
1967
1968 #[test]
1969 fn bayesian_belief_wire_format_recomputes_posterior() {
1970 let evidence = evidence();
1971 let mut belief = BayesianBelief::new(Probability::from_percent(10).unwrap());
1972 belief
1973 .observe(
1974 BayesianEvidence::new(
1975 evidence.id().clone(),
1976 Probability::from_percent(80).unwrap(),
1977 Probability::from_percent(20).unwrap(),
1978 "framework header observed",
1979 )
1980 .unwrap(),
1981 )
1982 .unwrap();
1983 let encoded = serde_json::to_value(&belief).unwrap();
1984 let decoded: BayesianBelief = serde_json::from_value(encoded.clone()).unwrap();
1985 assert_eq!(decoded, belief);
1986
1987 let mut tampered = encoded;
1988 tampered["posterior"] = serde_json::json!(500_000);
1989 assert!(serde_json::from_value::<BayesianBelief>(tampered).is_err());
1990
1991 let mut duplicated = serde_json::to_value(&belief).unwrap();
1992 let first_observation = duplicated["evidence"][0].clone();
1993 duplicated["evidence"]
1994 .as_array_mut()
1995 .unwrap()
1996 .push(first_observation);
1997 assert!(serde_json::from_value::<BayesianBelief>(duplicated).is_err());
1998 }
1999
2000 #[test]
2001 fn relation_deduplicates_evidence_provenance() {
2002 let evidence = evidence();
2003 let evidence_id = evidence.id().clone();
2004 let mut relation = KnowledgeRelation::new(
2005 EntityId::new("technology:php").unwrap(),
2006 EntityId::new("technology:laravel").unwrap(),
2007 RelationKind::Uses,
2008 ConfidenceScore::from_percent(82).unwrap(),
2009 evidence_id.clone(),
2010 );
2011
2012 relation.add_evidence(evidence_id);
2013
2014 assert_eq!(relation.evidence_ids().len(), 1);
2015 assert_eq!(relation.confidence().basis_points(), 8_200);
2016 }
2017
2018 #[test]
2019 fn required_names_reject_whitespace() {
2020 assert!(EntityId::new(" ").is_err());
2021 assert!(KnowledgePredicate::new("http", " ").is_err());
2022 assert!(EvidenceSource::new("", "header").is_err());
2023 assert!(EvidenceSource::new("http", "header")
2024 .unwrap()
2025 .with_correlation_id(" ")
2026 .is_err());
2027 assert!(KnowledgeEntity::new(subject(), EntityKind::Endpoint, " ").is_err());
2028 }
2029
2030 #[test]
2031 fn wire_format_cannot_bypass_non_empty_invariants() {
2032 let evidence = evidence();
2033 let mut fact = Fact::new(
2034 evidence.subject().clone(),
2035 evidence.predicate().clone(),
2036 evidence.value().clone(),
2037 ConfidenceScore::from_percent(90).unwrap(),
2038 evidence.id().clone(),
2039 );
2040 fact.evidence_ids.clear();
2041 assert!(serde_json::from_value::<Fact>(serde_json::to_value(fact).unwrap()).is_err());
2042
2043 let invalid_entity = serde_json::json!({
2044 "id": "endpoint:test",
2045 "kind": "endpoint",
2046 "label": " "
2047 });
2048 assert!(serde_json::from_value::<KnowledgeEntity>(invalid_entity).is_err());
2049
2050 let invalid_contribution = serde_json::json!({
2051 "evidence_id": evidence.id(),
2052 "direction": "supporting",
2053 "weight": 5_000,
2054 "rationale": " "
2055 });
2056 assert!(serde_json::from_value::<EvidenceContribution>(invalid_contribution).is_err());
2057 }
2058}