1use std::{any::Any, collections::HashMap, fmt, sync::Arc};
11
12use serde::{Deserialize, Serialize, de::DeserializeOwned};
13use thiserror::Error;
14
15use crate::context::ContextKey;
16use crate::types::{
17 ActorId, ApprovalId, ArtifactId, ContentHash, FactId, GateId, ObservationId, ProposalId,
18 SpanId, Timestamp, TraceId, TraceReference, TraceSystemId, UnitInterval, ValidationCheckId,
19};
20
21#[derive(Debug, Clone, PartialEq, Eq, Hash, Serialize, Deserialize)]
23pub struct FactFamilyId(String);
24
25impl FactFamilyId {
26 #[must_use]
28 pub fn new(value: impl Into<String>) -> Self {
29 Self(value.into())
30 }
31
32 #[must_use]
34 pub fn as_str(&self) -> &str {
35 &self.0
36 }
37}
38
39impl fmt::Display for FactFamilyId {
40 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
41 f.write_str(&self.0)
42 }
43}
44
45impl From<&'static str> for FactFamilyId {
46 fn from(value: &'static str) -> Self {
47 Self::new(value)
48 }
49}
50
51impl From<String> for FactFamilyId {
52 fn from(value: String) -> Self {
53 Self::new(value)
54 }
55}
56
57#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Serialize, Deserialize)]
59pub struct PayloadVersion(u16);
60
61impl PayloadVersion {
62 #[must_use]
64 pub const fn new(value: u16) -> Self {
65 Self(value)
66 }
67
68 #[must_use]
70 pub const fn get(self) -> u16 {
71 self.0
72 }
73}
74
75impl fmt::Display for PayloadVersion {
76 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
77 write!(f, "{}", self.0)
78 }
79}
80
81impl From<u16> for PayloadVersion {
82 fn from(value: u16) -> Self {
83 Self::new(value)
84 }
85}
86
87#[derive(Debug, Clone, PartialEq, Eq, Hash, Serialize, Deserialize)]
89pub struct Provenance(String);
90
91impl Provenance {
92 #[must_use]
94 pub fn new(value: impl Into<String>) -> Self {
95 Self(value.into())
96 }
97
98 #[must_use]
100 pub fn as_str(&self) -> &str {
101 &self.0
102 }
103}
104
105impl fmt::Display for Provenance {
106 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
107 f.write_str(&self.0)
108 }
109}
110
111impl From<&str> for Provenance {
112 fn from(value: &str) -> Self {
113 Self::new(value)
114 }
115}
116
117impl From<String> for Provenance {
118 fn from(value: String) -> Self {
119 Self::new(value)
120 }
121}
122
123pub trait ProvenanceSource: Copy + Send + Sync + 'static {
160 fn as_str(&self) -> &'static str;
164
165 #[must_use]
167 fn provenance(self) -> Provenance {
168 Provenance::from(self.as_str())
169 }
170
171 #[must_use]
174 fn proposed_fact<T>(
175 self,
176 key: ContextKey,
177 id: impl Into<ProposalId>,
178 payload: T,
179 ) -> ProposedFact
180 where
181 T: FactPayload + PartialEq,
182 {
183 ProposedFact::new(key, id, payload, self.provenance())
184 }
185}
186
187pub trait FactPayload: fmt::Debug + Clone + Serialize + Send + Sync + 'static {
192 const FAMILY: &'static str;
194 const VERSION: u16;
196
197 fn validate(&self) -> Result<(), PayloadError> {
200 Ok(())
201 }
202}
203
204trait ErasedFactPayload: fmt::Debug + Send + Sync {
205 fn family(&self) -> FactFamilyId;
206 fn version(&self) -> PayloadVersion;
207 fn validate(&self) -> Result<(), PayloadError>;
208 fn as_any(&self) -> &dyn Any;
209 fn to_json_value(&self) -> Result<serde_json::Value, PayloadError>;
210 fn equivalent(&self, other: &dyn ErasedFactPayload) -> bool;
211}
212
213impl<T> ErasedFactPayload for T
214where
215 T: FactPayload + PartialEq,
216{
217 fn family(&self) -> FactFamilyId {
218 FactFamilyId::from(T::FAMILY)
219 }
220
221 fn version(&self) -> PayloadVersion {
222 PayloadVersion::new(T::VERSION)
223 }
224
225 fn validate(&self) -> Result<(), PayloadError> {
226 FactPayload::validate(self)
227 }
228
229 fn as_any(&self) -> &dyn Any {
230 self
231 }
232
233 fn to_json_value(&self) -> Result<serde_json::Value, PayloadError> {
234 serde_json::to_value(self).map_err(|err| PayloadError::Serialize {
235 family: T::FAMILY.into(),
236 version: T::VERSION.into(),
237 reason: err.to_string(),
238 })
239 }
240
241 fn equivalent(&self, other: &dyn ErasedFactPayload) -> bool {
242 other.as_any().downcast_ref::<T>() == Some(self)
243 }
244}
245
246#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
249#[serde(deny_unknown_fields)]
250pub struct TextPayload {
251 text: String,
252}
253
254impl TextPayload {
255 #[must_use]
257 pub fn new(text: impl Into<String>) -> Self {
258 Self { text: text.into() }
259 }
260
261 #[must_use]
263 pub fn as_str(&self) -> &str {
264 &self.text
265 }
266}
267
268impl FactPayload for TextPayload {
269 const FAMILY: &'static str = "converge.text";
270 const VERSION: u16 = 1;
271}
272
273#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
275#[serde(deny_unknown_fields)]
276pub struct DiagnosticPayload {
277 source: String,
278 message: String,
279}
280
281impl DiagnosticPayload {
282 #[must_use]
284 pub fn new(source: impl Into<String>, message: impl Into<String>) -> Self {
285 Self {
286 source: source.into(),
287 message: message.into(),
288 }
289 }
290
291 #[must_use]
293 pub fn source(&self) -> &str {
294 &self.source
295 }
296
297 #[must_use]
299 pub fn message(&self) -> &str {
300 &self.message
301 }
302}
303
304impl FactPayload for DiagnosticPayload {
305 const FAMILY: &'static str = "converge.diagnostic";
306 const VERSION: u16 = 1;
307}
308
309#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
311#[serde(deny_unknown_fields)]
312pub struct ExecutionProducerIdentity {
313 pub name: String,
315 pub version: String,
317}
318
319impl ExecutionProducerIdentity {
320 #[must_use]
322 pub fn new(name: impl Into<String>, version: impl Into<String>) -> Self {
323 Self {
324 name: name.into(),
325 version: version.into(),
326 }
327 }
328
329 fn validate(&self) -> Result<(), String> {
330 validate_non_empty("producer.name", &self.name)?;
331 validate_non_empty("producer.version", &self.version)
332 }
333}
334
335#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
337#[serde(deny_unknown_fields)]
338pub struct NativeExecutionIdentity {
339 pub backend: String,
341 pub version: String,
343 pub source_url: String,
345 pub expected_commit: String,
347 pub actual_commit: String,
349 pub source_mode: String,
351}
352
353impl NativeExecutionIdentity {
354 #[must_use]
356 pub fn new(
357 backend: impl Into<String>,
358 version: impl Into<String>,
359 source_url: impl Into<String>,
360 expected_commit: impl Into<String>,
361 actual_commit: impl Into<String>,
362 source_mode: impl Into<String>,
363 ) -> Self {
364 Self {
365 backend: backend.into(),
366 version: version.into(),
367 source_url: source_url.into(),
368 expected_commit: expected_commit.into(),
369 actual_commit: actual_commit.into(),
370 source_mode: source_mode.into(),
371 }
372 }
373
374 fn validate(&self) -> Result<(), String> {
375 validate_non_empty("native_identity.backend", &self.backend)?;
376 validate_non_empty("native_identity.version", &self.version)?;
377 validate_non_empty("native_identity.source_url", &self.source_url)?;
378 validate_non_empty("native_identity.expected_commit", &self.expected_commit)?;
379 validate_non_empty("native_identity.actual_commit", &self.actual_commit)?;
380 validate_non_empty("native_identity.source_mode", &self.source_mode)
381 }
382}
383
384#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
391#[serde(deny_unknown_fields)]
392pub struct ExecutionIdentity {
393 pub producer: ExecutionProducerIdentity,
395 pub backend: String,
397 pub backend_version: String,
399 pub build_identity: String,
401 pub runtime_config: String,
403 pub native_identity: Option<NativeExecutionIdentity>,
405}
406
407impl ExecutionIdentity {
408 #[must_use]
410 pub fn new(
411 producer: ExecutionProducerIdentity,
412 backend: impl Into<String>,
413 backend_version: impl Into<String>,
414 build_identity: impl Into<String>,
415 runtime_config: impl Into<String>,
416 native_identity: Option<NativeExecutionIdentity>,
417 ) -> Self {
418 Self {
419 producer,
420 backend: backend.into(),
421 backend_version: backend_version.into(),
422 build_identity: build_identity.into(),
423 runtime_config: runtime_config.into(),
424 native_identity,
425 }
426 }
427
428 #[must_use]
430 pub fn non_native(
431 producer_name: impl Into<String>,
432 producer_version: impl Into<String>,
433 backend: impl Into<String>,
434 runtime_config: impl Into<String>,
435 ) -> Self {
436 Self::new(
437 ExecutionProducerIdentity::new(producer_name, producer_version),
438 backend,
439 "not_applicable",
440 "not_applicable",
441 runtime_config,
442 None,
443 )
444 }
445
446 #[must_use]
448 pub fn unspecified(
449 producer_name: impl Into<String>,
450 producer_version: impl Into<String>,
451 ) -> Self {
452 Self::new(
453 ExecutionProducerIdentity::new(producer_name, producer_version),
454 "unknown",
455 "unknown",
456 "unknown",
457 "unknown",
458 None,
459 )
460 }
461
462 #[must_use]
480 pub fn runtime_config_from_typed<T: Serialize>(value: &T) -> String {
481 serde_json::to_string(value)
482 .expect("typed runtime_config must serialize to JSON; check Serialize impl")
483 }
484
485 #[must_use]
489 pub fn with_runtime_config_typed<T: Serialize>(mut self, value: &T) -> Self {
490 self.runtime_config = Self::runtime_config_from_typed(value);
491 self
492 }
493
494 fn validate(&self) -> Result<(), String> {
495 self.producer.validate()?;
496 validate_non_empty("backend", &self.backend)?;
497 validate_non_empty("backend_version", &self.backend_version)?;
498 validate_non_empty("build_identity", &self.build_identity)?;
499 validate_non_empty("runtime_config", &self.runtime_config)?;
500 if let Some(native_identity) = &self.native_identity {
501 native_identity.validate()?;
502 }
503 Ok(())
504 }
505}
506
507#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
509#[serde(deny_unknown_fields)]
510pub struct ExecutionIdentityEvidence {
511 pub subject_key: ContextKey,
513 pub subject_id: String,
515 pub subject_family: FactFamilyId,
517 pub subject_version: PayloadVersion,
519 pub identity: ExecutionIdentity,
521}
522
523impl ExecutionIdentityEvidence {
524 #[must_use]
526 pub fn new(
527 subject_key: ContextKey,
528 subject_id: impl Into<String>,
529 subject_family: impl Into<FactFamilyId>,
530 subject_version: impl Into<PayloadVersion>,
531 identity: ExecutionIdentity,
532 ) -> Self {
533 Self {
534 subject_key,
535 subject_id: subject_id.into(),
536 subject_family: subject_family.into(),
537 subject_version: subject_version.into(),
538 identity,
539 }
540 }
541
542 #[must_use]
544 pub fn for_payload<T: FactPayload>(
545 subject_key: ContextKey,
546 subject_id: impl Into<String>,
547 identity: ExecutionIdentity,
548 ) -> Self {
549 Self::new(subject_key, subject_id, T::FAMILY, T::VERSION, identity)
550 }
551}
552
553impl FactPayload for ExecutionIdentityEvidence {
554 const FAMILY: &'static str = "converge.execution_identity.evidence";
555 const VERSION: u16 = 1;
556
557 fn validate(&self) -> Result<(), PayloadError> {
558 validate_non_empty("subject_id", &self.subject_id).map_err(|reason| {
559 PayloadError::Invalid {
560 family: Self::FAMILY.into(),
561 version: Self::VERSION.into(),
562 reason,
563 }
564 })?;
565 self.identity
566 .validate()
567 .map_err(|reason| PayloadError::Invalid {
568 family: Self::FAMILY.into(),
569 version: Self::VERSION.into(),
570 reason,
571 })
572 }
573}
574
575fn validate_non_empty(field: &str, value: &str) -> Result<(), String> {
576 if value.trim().is_empty() {
577 Err(format!("{field} must not be empty"))
578 } else {
579 Ok(())
580 }
581}
582
583#[derive(Debug, Clone, PartialEq, Eq, Error)]
585pub enum PayloadError {
586 #[error("invalid payload for {family} v{version}: {reason}")]
588 Invalid {
589 family: FactFamilyId,
591 version: PayloadVersion,
593 reason: String,
595 },
596 #[error("failed to serialize payload {family} v{version}: {reason}")]
598 Serialize {
599 family: FactFamilyId,
601 version: PayloadVersion,
603 reason: String,
605 },
606 #[error("failed to deserialize payload {family} v{version}: {reason}")]
608 Deserialize {
609 family: FactFamilyId,
611 version: PayloadVersion,
613 reason: String,
615 },
616 #[error("unknown payload family/version: {family} v{version}")]
618 UnknownFamilyVersion {
619 family: FactFamilyId,
621 version: PayloadVersion,
623 },
624 #[error(
626 "payload type mismatch: expected {expected} v{expected_version}, got {actual} v{actual_version}"
627 )]
628 TypeMismatch {
629 expected: FactFamilyId,
631 expected_version: PayloadVersion,
633 actual: FactFamilyId,
635 actual_version: PayloadVersion,
637 },
638}
639
640#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
643#[serde(deny_unknown_fields)]
644pub struct WireFactPayload {
645 pub family: FactFamilyId,
647 pub version: PayloadVersion,
649 pub payload: serde_json::Value,
651}
652
653impl WireFactPayload {
654 fn from_erased(payload: &dyn ErasedFactPayload) -> Result<Self, PayloadError> {
655 Ok(Self {
656 family: payload.family(),
657 version: payload.version(),
658 payload: payload.to_json_value()?,
659 })
660 }
661}
662
663#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
666#[serde(deny_unknown_fields)]
667pub struct WireProposedFact {
668 pub key: ContextKey,
670 pub id: ProposalId,
672 pub payload: WireFactPayload,
674 pub confidence: UnitInterval,
676 pub provenance: Provenance,
678}
679
680#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
682#[serde(deny_unknown_fields)]
683pub struct WireContextFact {
684 pub key: ContextKey,
686 pub id: FactId,
688 pub payload: WireFactPayload,
690 pub promotion_record: FactPromotionRecord,
692 pub created_at: Timestamp,
694}
695
696type PayloadDecoder = Box<
697 dyn Fn(serde_json::Value) -> Result<Arc<dyn ErasedFactPayload>, PayloadError> + Send + Sync,
698>;
699
700#[derive(Default)]
703pub struct PayloadRegistry {
704 decoders: HashMap<(FactFamilyId, PayloadVersion), PayloadDecoder>,
705}
706
707impl PayloadRegistry {
708 #[must_use]
710 pub fn new() -> Self {
711 Self::default()
712 }
713
714 #[must_use]
716 pub fn with_pack_payloads() -> Self {
717 let mut registry = Self::new();
718 registry.register::<TextPayload>();
719 registry.register::<DiagnosticPayload>();
720 registry.register::<ExecutionIdentityEvidence>();
721 registry
722 }
723
724 pub fn register<T>(&mut self)
726 where
727 T: FactPayload + PartialEq + DeserializeOwned,
728 {
729 self.decoders.insert(
730 (
731 FactFamilyId::from(T::FAMILY),
732 PayloadVersion::new(T::VERSION),
733 ),
734 Box::new(|value| {
735 let payload: T =
736 serde_json::from_value(value).map_err(|err| PayloadError::Deserialize {
737 family: T::FAMILY.into(),
738 version: T::VERSION.into(),
739 reason: err.to_string(),
740 })?;
741 payload.validate()?;
742 Ok(Arc::new(payload))
743 }),
744 );
745 }
746
747 fn decode(
748 &self,
749 family: &FactFamilyId,
750 version: PayloadVersion,
751 payload: serde_json::Value,
752 ) -> Result<Arc<dyn ErasedFactPayload>, PayloadError> {
753 let decoder = self
754 .decoders
755 .get(&(family.clone(), version))
756 .ok_or_else(|| PayloadError::UnknownFamilyVersion {
757 family: family.clone(),
758 version,
759 })?;
760 decoder(payload)
761 }
762}
763
764#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
766pub enum FactActorKind {
767 Human,
769 Suggestor,
771 System,
773}
774
775#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
777pub struct FactActor {
778 id: ActorId,
779 kind: FactActorKind,
780}
781
782impl FactActor {
783 #[must_use]
785 pub fn id(&self) -> &ActorId {
786 &self.id
787 }
788
789 #[must_use]
791 pub fn kind(&self) -> FactActorKind {
792 self.kind
793 }
794
795 #[doc(hidden)]
796 pub fn new_projection(id: impl Into<ActorId>, kind: FactActorKind) -> Self {
797 Self {
798 id: id.into(),
799 kind,
800 }
801 }
802}
803
804#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize, Default)]
806pub struct FactValidationSummary {
807 checks_passed: Vec<ValidationCheckId>,
808 checks_skipped: Vec<ValidationCheckId>,
809 warnings: Vec<String>,
810}
811
812impl FactValidationSummary {
813 #[must_use]
815 pub fn checks_passed(&self) -> &[ValidationCheckId] {
816 &self.checks_passed
817 }
818
819 #[must_use]
821 pub fn checks_skipped(&self) -> &[ValidationCheckId] {
822 &self.checks_skipped
823 }
824
825 #[must_use]
827 pub fn warnings(&self) -> &[String] {
828 &self.warnings
829 }
830
831 #[doc(hidden)]
832 pub fn new_projection(
833 checks_passed: Vec<ValidationCheckId>,
834 checks_skipped: Vec<ValidationCheckId>,
835 warnings: Vec<String>,
836 ) -> Self {
837 Self {
838 checks_passed,
839 checks_skipped,
840 warnings,
841 }
842 }
843}
844
845#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
847#[serde(tag = "type", content = "id")]
848pub enum FactEvidenceRef {
849 Observation(ObservationId),
851 HumanApproval(ApprovalId),
853 Derived(ArtifactId),
855}
856
857#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
859pub struct FactLocalTrace {
860 trace_id: TraceId,
861 span_id: SpanId,
862 parent_span_id: Option<SpanId>,
863 sampled: bool,
864}
865
866impl FactLocalTrace {
867 #[must_use]
869 pub fn trace_id(&self) -> &TraceId {
870 &self.trace_id
871 }
872
873 #[must_use]
875 pub fn span_id(&self) -> &SpanId {
876 &self.span_id
877 }
878
879 #[must_use]
881 pub fn parent_span_id(&self) -> Option<&SpanId> {
882 self.parent_span_id.as_ref()
883 }
884
885 #[must_use]
887 pub fn sampled(&self) -> bool {
888 self.sampled
889 }
890
891 #[doc(hidden)]
892 pub fn new_projection(
893 trace_id: impl Into<TraceId>,
894 span_id: impl Into<SpanId>,
895 parent_span_id: Option<SpanId>,
896 sampled: bool,
897 ) -> Self {
898 Self {
899 trace_id: trace_id.into(),
900 span_id: span_id.into(),
901 parent_span_id,
902 sampled,
903 }
904 }
905}
906
907#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
909pub struct FactRemoteTrace {
910 system: TraceSystemId,
911 reference: TraceReference,
912 retrieval_auth: Option<String>,
913 retention_hint: Option<String>,
914}
915
916impl FactRemoteTrace {
917 #[must_use]
919 pub fn system(&self) -> &TraceSystemId {
920 &self.system
921 }
922
923 #[must_use]
925 pub fn reference(&self) -> &TraceReference {
926 &self.reference
927 }
928
929 #[must_use]
931 pub fn retrieval_auth(&self) -> Option<&str> {
932 self.retrieval_auth.as_deref()
933 }
934
935 #[must_use]
937 pub fn retention_hint(&self) -> Option<&str> {
938 self.retention_hint.as_deref()
939 }
940
941 #[doc(hidden)]
942 pub fn new_projection(
943 system: impl Into<TraceSystemId>,
944 reference: impl Into<TraceReference>,
945 retrieval_auth: Option<String>,
946 retention_hint: Option<String>,
947 ) -> Self {
948 Self {
949 system: system.into(),
950 reference: reference.into(),
951 retrieval_auth,
952 retention_hint,
953 }
954 }
955}
956
957#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
959#[serde(tag = "type")]
960pub enum FactTraceLink {
961 Local(FactLocalTrace),
963 Remote(FactRemoteTrace),
965}
966
967impl FactTraceLink {
968 #[must_use]
970 pub fn is_replay_eligible(&self) -> bool {
971 matches!(self, Self::Local(_))
972 }
973}
974
975#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
977pub struct FactPromotionRecord {
978 gate_id: GateId,
979 policy_version_hash: ContentHash,
980 approver: FactActor,
981 validation_summary: FactValidationSummary,
982 evidence_refs: Vec<FactEvidenceRef>,
983 trace_link: FactTraceLink,
984 promoted_at: Timestamp,
985}
986
987impl FactPromotionRecord {
988 #[must_use]
990 pub fn gate_id(&self) -> &GateId {
991 &self.gate_id
992 }
993
994 #[must_use]
996 pub fn policy_version_hash(&self) -> &ContentHash {
997 &self.policy_version_hash
998 }
999
1000 #[must_use]
1002 pub fn approver(&self) -> &FactActor {
1003 &self.approver
1004 }
1005
1006 #[must_use]
1008 pub fn validation_summary(&self) -> &FactValidationSummary {
1009 &self.validation_summary
1010 }
1011
1012 #[must_use]
1014 pub fn evidence_refs(&self) -> &[FactEvidenceRef] {
1015 &self.evidence_refs
1016 }
1017
1018 #[must_use]
1020 pub fn trace_link(&self) -> &FactTraceLink {
1021 &self.trace_link
1022 }
1023
1024 #[must_use]
1026 pub fn promoted_at(&self) -> &Timestamp {
1027 &self.promoted_at
1028 }
1029
1030 #[must_use]
1032 pub fn is_replay_eligible(&self) -> bool {
1033 self.trace_link.is_replay_eligible()
1034 }
1035
1036 #[doc(hidden)]
1037 pub fn new_projection(
1038 gate_id: impl Into<GateId>,
1039 policy_version_hash: ContentHash,
1040 approver: FactActor,
1041 validation_summary: FactValidationSummary,
1042 evidence_refs: Vec<FactEvidenceRef>,
1043 trace_link: FactTraceLink,
1044 promoted_at: impl Into<Timestamp>,
1045 ) -> Self {
1046 Self {
1047 gate_id: gate_id.into(),
1048 policy_version_hash,
1049 approver,
1050 validation_summary,
1051 evidence_refs,
1052 trace_link,
1053 promoted_at: promoted_at.into(),
1054 }
1055 }
1056}
1057
1058#[derive(Clone)]
1065pub struct ContextFact {
1066 key: ContextKey,
1068 id: FactId,
1070 payload: Arc<dyn ErasedFactPayload>,
1072 promotion_record: FactPromotionRecord,
1074 created_at: Timestamp,
1076}
1077
1078impl fmt::Debug for ContextFact {
1079 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
1080 f.debug_struct("ContextFact")
1081 .field("key", &self.key)
1082 .field("id", &self.id)
1083 .field("payload_family", &self.payload_family())
1084 .field("payload_version", &self.payload_version())
1085 .field("promotion_record", &self.promotion_record)
1086 .field("created_at", &self.created_at)
1087 .finish()
1088 }
1089}
1090
1091impl PartialEq for ContextFact {
1092 fn eq(&self, other: &Self) -> bool {
1093 self.key == other.key
1094 && self.id == other.id
1095 && self.payload_family() == other.payload_family()
1096 && self.payload_version() == other.payload_version()
1097 && self.payload.equivalent(other.payload.as_ref())
1098 && self.promotion_record == other.promotion_record
1099 && self.created_at == other.created_at
1100 }
1101}
1102
1103impl Eq for ContextFact {}
1104
1105impl Serialize for ContextFact {
1106 fn serialize<S>(&self, serializer: S) -> Result<S::Ok, S::Error>
1107 where
1108 S: serde::Serializer,
1109 {
1110 self.to_wire()
1111 .map_err(serde::ser::Error::custom)?
1112 .serialize(serializer)
1113 }
1114}
1115
1116impl ContextFact {
1117 #[must_use]
1123 pub fn new_projection<T>(
1124 key: ContextKey,
1125 id: impl Into<FactId>,
1126 payload: T,
1127 promotion_record: FactPromotionRecord,
1128 created_at: impl Into<Timestamp>,
1129 ) -> Self
1130 where
1131 T: FactPayload + PartialEq,
1132 {
1133 Self {
1134 key,
1135 id: id.into(),
1136 payload: Arc::new(payload),
1137 promotion_record,
1138 created_at: created_at.into(),
1139 }
1140 }
1141
1142 pub fn from_wire(
1145 wire: WireContextFact,
1146 registry: &PayloadRegistry,
1147 ) -> Result<Self, PayloadError> {
1148 let payload = registry.decode(
1149 &wire.payload.family,
1150 wire.payload.version,
1151 wire.payload.payload,
1152 )?;
1153 Ok(Self {
1154 key: wire.key,
1155 id: wire.id,
1156 payload,
1157 promotion_record: wire.promotion_record,
1158 created_at: wire.created_at,
1159 })
1160 }
1161
1162 pub fn to_wire(&self) -> Result<WireContextFact, PayloadError> {
1164 Ok(WireContextFact {
1165 key: self.key,
1166 id: self.id.clone(),
1167 payload: WireFactPayload::from_erased(self.payload.as_ref())?,
1168 promotion_record: self.promotion_record.clone(),
1169 created_at: self.created_at.clone(),
1170 })
1171 }
1172
1173 #[must_use]
1175 pub fn key(&self) -> ContextKey {
1176 self.key
1177 }
1178
1179 #[must_use]
1181 pub fn id(&self) -> &FactId {
1182 &self.id
1183 }
1184
1185 #[must_use]
1188 pub fn payload<T: FactPayload>(&self) -> Option<&T> {
1189 self.payload.as_any().downcast_ref::<T>()
1190 }
1191
1192 pub fn require_payload<T: FactPayload>(&self) -> Result<&T, PayloadError> {
1194 self.payload::<T>()
1195 .ok_or_else(|| PayloadError::TypeMismatch {
1196 expected: T::FAMILY.into(),
1197 expected_version: T::VERSION.into(),
1198 actual: self.payload_family(),
1199 actual_version: self.payload_version(),
1200 })
1201 }
1202
1203 #[must_use]
1205 pub fn payload_family(&self) -> FactFamilyId {
1206 self.payload.family()
1207 }
1208
1209 #[must_use]
1211 pub fn payload_version(&self) -> PayloadVersion {
1212 self.payload.version()
1213 }
1214
1215 #[must_use]
1217 pub fn text(&self) -> Option<&str> {
1218 self.payload::<TextPayload>().map(TextPayload::as_str)
1219 }
1220
1221 pub fn validate_payload(&self) -> Result<(), PayloadError> {
1223 self.payload.validate()
1224 }
1225
1226 #[must_use]
1228 pub fn promotion_record(&self) -> &FactPromotionRecord {
1229 &self.promotion_record
1230 }
1231
1232 #[must_use]
1234 pub fn created_at(&self) -> &Timestamp {
1235 &self.created_at
1236 }
1237
1238 #[must_use]
1240 pub fn is_replay_eligible(&self) -> bool {
1241 self.promotion_record.is_replay_eligible()
1242 }
1243}
1244
1245#[derive(Clone)]
1250pub struct ProposedFact {
1251 pub key: ContextKey,
1253 pub id: ProposalId,
1255 payload: Arc<dyn ErasedFactPayload>,
1257 confidence: UnitInterval,
1259 pub provenance: Provenance,
1261}
1262
1263impl fmt::Debug for ProposedFact {
1264 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
1265 f.debug_struct("ProposedFact")
1266 .field("key", &self.key)
1267 .field("id", &self.id)
1268 .field("payload_family", &self.payload_family())
1269 .field("payload_version", &self.payload_version())
1270 .field("confidence", &self.confidence)
1271 .field("provenance", &self.provenance)
1272 .finish()
1273 }
1274}
1275
1276impl PartialEq for ProposedFact {
1277 fn eq(&self, other: &Self) -> bool {
1278 self.key == other.key
1279 && self.id == other.id
1280 && self.payload_family() == other.payload_family()
1281 && self.payload_version() == other.payload_version()
1282 && self.payload.equivalent(other.payload.as_ref())
1283 && self.confidence == other.confidence
1284 && self.provenance == other.provenance
1285 }
1286}
1287
1288impl Serialize for ProposedFact {
1289 fn serialize<S>(&self, serializer: S) -> Result<S::Ok, S::Error>
1290 where
1291 S: serde::Serializer,
1292 {
1293 self.to_wire()
1294 .map_err(serde::ser::Error::custom)?
1295 .serialize(serializer)
1296 }
1297}
1298
1299impl ProposedFact {
1300 #[must_use]
1304 pub fn new<T>(
1305 key: ContextKey,
1306 id: impl Into<ProposalId>,
1307 payload: T,
1308 provenance: impl Into<Provenance>,
1309 ) -> Self
1310 where
1311 T: FactPayload + PartialEq,
1312 {
1313 Self {
1314 key,
1315 id: id.into(),
1316 payload: Arc::new(payload),
1317 confidence: UnitInterval::ONE,
1318 provenance: provenance.into(),
1319 }
1320 }
1321
1322 pub fn from_wire(
1325 wire: WireProposedFact,
1326 registry: &PayloadRegistry,
1327 ) -> Result<Self, PayloadError> {
1328 let payload = registry.decode(
1329 &wire.payload.family,
1330 wire.payload.version,
1331 wire.payload.payload,
1332 )?;
1333 Ok(Self {
1334 key: wire.key,
1335 id: wire.id,
1336 payload,
1337 confidence: wire.confidence,
1338 provenance: wire.provenance,
1339 })
1340 }
1341
1342 pub fn to_wire(&self) -> Result<WireProposedFact, PayloadError> {
1344 Ok(WireProposedFact {
1345 key: self.key,
1346 id: self.id.clone(),
1347 payload: WireFactPayload::from_erased(self.payload.as_ref())?,
1348 confidence: self.confidence,
1349 provenance: self.provenance.clone(),
1350 })
1351 }
1352
1353 #[must_use]
1360 pub fn to_context_fact(
1361 &self,
1362 id: impl Into<FactId>,
1363 promotion_record: FactPromotionRecord,
1364 created_at: impl Into<Timestamp>,
1365 ) -> ContextFact {
1366 ContextFact {
1367 key: self.key,
1368 id: id.into(),
1369 payload: Arc::clone(&self.payload),
1370 promotion_record,
1371 created_at: created_at.into(),
1372 }
1373 }
1374
1375 #[must_use]
1377 pub fn key(&self) -> ContextKey {
1378 self.key
1379 }
1380
1381 #[must_use]
1383 pub fn id(&self) -> &ProposalId {
1384 &self.id
1385 }
1386
1387 #[must_use]
1390 pub fn payload<T: FactPayload>(&self) -> Option<&T> {
1391 self.payload.as_any().downcast_ref::<T>()
1392 }
1393
1394 pub fn require_payload<T: FactPayload>(&self) -> Result<&T, PayloadError> {
1396 self.payload::<T>()
1397 .ok_or_else(|| PayloadError::TypeMismatch {
1398 expected: T::FAMILY.into(),
1399 expected_version: T::VERSION.into(),
1400 actual: self.payload_family(),
1401 actual_version: self.payload_version(),
1402 })
1403 }
1404
1405 #[must_use]
1407 pub fn payload_family(&self) -> FactFamilyId {
1408 self.payload.family()
1409 }
1410
1411 #[must_use]
1413 pub fn payload_version(&self) -> PayloadVersion {
1414 self.payload.version()
1415 }
1416
1417 #[must_use]
1419 pub fn text(&self) -> Option<&str> {
1420 self.payload::<TextPayload>().map(TextPayload::as_str)
1421 }
1422
1423 pub fn validate_payload(&self) -> Result<(), PayloadError> {
1425 self.payload.validate()
1426 }
1427
1428 #[must_use]
1430 pub fn provenance_ref(&self) -> &Provenance {
1431 &self.provenance
1432 }
1433
1434 #[must_use]
1436 pub fn provenance(&self) -> &str {
1437 self.provenance.as_str()
1438 }
1439
1440 #[must_use]
1442 pub fn confidence(&self) -> f64 {
1443 self.confidence.as_f64()
1444 }
1445
1446 #[must_use]
1454 pub fn with_confidence(mut self, confidence: f64) -> Self {
1455 self.confidence = UnitInterval::clamped(confidence);
1456 self
1457 }
1458
1459 #[must_use]
1477 pub fn adjust_confidence(mut self, delta: f64) -> Self {
1478 self.confidence = self.confidence.saturating_add(delta);
1479 self
1480 }
1481}
1482
1483pub const CONFIDENCE_STEP_TINY: f64 = 0.05;
1485
1486pub const CONFIDENCE_STEP_MINOR: f64 = 0.1;
1488
1489pub const CONFIDENCE_STEP_MEDIUM: f64 = 0.15;
1491
1492pub const CONFIDENCE_STEP_MAJOR: f64 = 0.2;
1494
1495pub const CONFIDENCE_STEP_PRIMARY: f64 = 0.25;
1497
1498#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
1500pub struct ValidationError {
1501 pub reason: String,
1503}
1504
1505impl std::fmt::Display for ValidationError {
1506 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
1507 write!(f, "validation failed: {}", self.reason)
1508 }
1509}
1510
1511impl std::error::Error for ValidationError {}
1512
1513#[cfg(test)]
1514mod tests {
1515 use super::*;
1516
1517 #[derive(Clone, Copy, Debug)]
1518 struct TestProvenance;
1519
1520 impl ProvenanceSource for TestProvenance {
1521 fn as_str(&self) -> &'static str {
1522 "test-provenance"
1523 }
1524 }
1525
1526 fn projection_record() -> FactPromotionRecord {
1527 FactPromotionRecord::new_projection(
1528 "projection-test",
1529 ContentHash::from_hex(
1530 "1111111111111111111111111111111111111111111111111111111111111111",
1531 ),
1532 FactActor::new_projection("actor-1", FactActorKind::System),
1533 FactValidationSummary::default(),
1534 Vec::new(),
1535 FactTraceLink::Local(FactLocalTrace::new_projection(
1536 "trace-1", "span-1", None, true,
1537 )),
1538 Timestamp::epoch(),
1539 )
1540 }
1541
1542 fn projection_fact(
1543 key: ContextKey,
1544 id: impl Into<FactId>,
1545 content: impl Into<String>,
1546 ) -> ContextFact {
1547 ContextFact::new_projection(
1548 key,
1549 id,
1550 TextPayload::new(content),
1551 projection_record(),
1552 Timestamp::epoch(),
1553 )
1554 }
1555
1556 #[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
1557 #[serde(deny_unknown_fields)]
1558 struct TestPayload {
1559 kind: String,
1560 score: f64,
1561 }
1562
1563 impl FactPayload for TestPayload {
1564 const FAMILY: &'static str = "test.payload";
1565 const VERSION: u16 = 1;
1566 }
1567
1568 fn native_identity() -> NativeExecutionIdentity {
1569 NativeExecutionIdentity::new(
1570 "CVC5",
1571 "1.3.3",
1572 "https://github.com/cvc5/cvc5",
1573 "expected",
1574 "actual",
1575 "vendored",
1576 )
1577 }
1578
1579 #[test]
1580 fn execution_identity_evidence_targets_typed_payload() {
1581 let identity = ExecutionIdentity::new(
1582 ExecutionProducerIdentity::new("soter", "0.1.0"),
1583 "cvc5",
1584 "1.3.3",
1585 "configure_flags=--no-poly",
1586 "timeout_ms=5000",
1587 Some(native_identity()),
1588 );
1589 let evidence = ExecutionIdentityEvidence::for_payload::<TestPayload>(
1590 ContextKey::Evaluations,
1591 "smt-report-q1",
1592 identity,
1593 );
1594
1595 assert_eq!(evidence.subject_key, ContextKey::Evaluations);
1596 assert_eq!(evidence.subject_id, "smt-report-q1");
1597 assert_eq!(evidence.subject_family, FactFamilyId::from("test.payload"));
1598 assert_eq!(evidence.subject_version, PayloadVersion::new(1));
1599 assert_eq!(evidence.identity.backend, "cvc5");
1600 assert!(FactPayload::validate(&evidence).is_ok());
1601 }
1602
1603 #[test]
1604 fn execution_identity_evidence_rejects_empty_subject_id() {
1605 let evidence = ExecutionIdentityEvidence::for_payload::<TestPayload>(
1606 ContextKey::Strategies,
1607 "",
1608 ExecutionIdentity::non_native("ferrox", "0.5.1", "greedy", "tasks=3"),
1609 );
1610
1611 assert!(matches!(
1612 FactPayload::validate(&evidence),
1613 Err(PayloadError::Invalid { .. })
1614 ));
1615 }
1616
1617 #[test]
1618 fn trace_link_local_is_replay_eligible() {
1619 let local = FactTraceLink::Local(FactLocalTrace {
1620 trace_id: "t1".into(),
1621 span_id: "s1".into(),
1622 parent_span_id: None,
1623 sampled: true,
1624 });
1625 assert!(local.is_replay_eligible());
1626 }
1627
1628 #[test]
1629 fn trace_link_remote_is_not_replay_eligible() {
1630 let remote = FactTraceLink::Remote(FactRemoteTrace {
1631 system: "datadog".into(),
1632 reference: "ref-1".into(),
1633 retrieval_auth: None,
1634 retention_hint: None,
1635 });
1636 assert!(!remote.is_replay_eligible());
1637 }
1638
1639 #[test]
1640 fn promotion_record_delegates_replay_eligibility() {
1641 let local_record = FactPromotionRecord::new_projection(
1642 "gate-1",
1643 ContentHash::from_hex(
1644 "1111111111111111111111111111111111111111111111111111111111111111",
1645 ),
1646 FactActor::new_projection("actor-1", FactActorKind::Human),
1647 FactValidationSummary::default(),
1648 Vec::new(),
1649 FactTraceLink::Local(FactLocalTrace::new_projection("t1", "s1", None, true)),
1650 "2026-01-01T00:00:00Z",
1651 );
1652 assert!(local_record.is_replay_eligible());
1653
1654 let remote_record = FactPromotionRecord::new_projection(
1655 "gate-2",
1656 ContentHash::from_hex(
1657 "2222222222222222222222222222222222222222222222222222222222222222",
1658 ),
1659 FactActor::new_projection("actor-2", FactActorKind::System),
1660 FactValidationSummary::default(),
1661 Vec::new(),
1662 FactTraceLink::Remote(FactRemoteTrace::new_projection("dd", "ref-1", None, None)),
1663 "2026-01-01T00:00:00Z",
1664 );
1665 assert!(!remote_record.is_replay_eligible());
1666 }
1667
1668 #[test]
1669 fn fact_delegates_replay_eligibility() {
1670 let fact = projection_fact(ContextKey::Seeds, "f1", "content");
1671 assert!(fact.is_replay_eligible());
1672 }
1673
1674 #[test]
1675 fn proposed_fact_new_sets_fields() {
1676 let pf = ProposedFact::new(
1677 ContextKey::Hypotheses,
1678 "p1",
1679 TextPayload::new("my content"),
1680 TestProvenance.provenance(),
1681 );
1682 assert_eq!(pf.key, ContextKey::Hypotheses);
1683 assert_eq!(pf.id, "p1");
1684 assert_eq!(pf.text(), Some("my content"));
1685 assert_eq!(pf.confidence(), 1.0);
1686 assert_eq!(pf.provenance(), "test-provenance");
1687 }
1688
1689 #[test]
1690 fn proposed_fact_with_confidence() {
1691 let pf = ProposedFact::new(
1692 ContextKey::Signals,
1693 "p2",
1694 TextPayload::new("c"),
1695 TestProvenance.provenance(),
1696 )
1697 .with_confidence(0.42);
1698 assert!((pf.confidence() - 0.42).abs() < f64::EPSILON);
1699 }
1700
1701 #[test]
1702 fn adjust_confidence_accumulates() {
1703 let pf = ProposedFact::new(
1704 ContextKey::Seeds,
1705 "p",
1706 TextPayload::new("c"),
1707 TestProvenance.provenance(),
1708 )
1709 .with_confidence(0.5)
1710 .adjust_confidence(CONFIDENCE_STEP_MINOR)
1711 .adjust_confidence(CONFIDENCE_STEP_MAJOR);
1712 assert!((pf.confidence() - 0.8).abs() < f64::EPSILON);
1713 }
1714
1715 #[test]
1716 fn adjust_confidence_clamps_at_one() {
1717 let pf = ProposedFact::new(
1718 ContextKey::Seeds,
1719 "p",
1720 TextPayload::new("c"),
1721 TestProvenance.provenance(),
1722 )
1723 .with_confidence(0.9)
1724 .adjust_confidence(CONFIDENCE_STEP_MAJOR);
1725 assert_eq!(pf.confidence(), 1.0);
1726 }
1727
1728 #[test]
1729 fn adjust_confidence_clamps_at_zero() {
1730 let pf = ProposedFact::new(
1731 ContextKey::Seeds,
1732 "p",
1733 TextPayload::new("c"),
1734 TestProvenance.provenance(),
1735 )
1736 .with_confidence(0.1)
1737 .adjust_confidence(-0.5);
1738 assert_eq!(pf.confidence(), 0.0);
1739 }
1740
1741 #[test]
1742 fn with_confidence_clamps_high() {
1743 let pf = ProposedFact::new(
1744 ContextKey::Seeds,
1745 "p",
1746 TextPayload::new("c"),
1747 TestProvenance.provenance(),
1748 )
1749 .with_confidence(1.5);
1750 assert_eq!(pf.confidence(), 1.0);
1751 }
1752
1753 #[test]
1754 fn with_confidence_clamps_negative() {
1755 let pf = ProposedFact::new(
1756 ContextKey::Seeds,
1757 "p",
1758 TextPayload::new("c"),
1759 TestProvenance.provenance(),
1760 )
1761 .with_confidence(-0.1);
1762 assert_eq!(pf.confidence(), 0.0);
1763 }
1764
1765 #[test]
1766 fn with_confidence_normalizes_nan() {
1767 let pf = ProposedFact::new(
1768 ContextKey::Seeds,
1769 "p",
1770 TextPayload::new("c"),
1771 TestProvenance.provenance(),
1772 )
1773 .with_confidence(f64::NAN);
1774 assert_eq!(pf.confidence(), 0.0);
1775 }
1776
1777 #[test]
1778 fn with_confidence_normalizes_infinity() {
1779 let pf = ProposedFact::new(
1780 ContextKey::Seeds,
1781 "p",
1782 TextPayload::new("c"),
1783 TestProvenance.provenance(),
1784 )
1785 .with_confidence(f64::INFINITY);
1786 assert_eq!(pf.confidence(), 0.0);
1787 }
1788
1789 #[test]
1790 fn wire_proposed_fact_deserialization_rejects_out_of_range_confidence() {
1791 let json = r#"{
1792 "key":"Seeds",
1793 "id":"p",
1794 "payload":{
1795 "family":"converge.text",
1796 "version":1,
1797 "payload":{"text":"c"}
1798 },
1799 "confidence":1.5,
1800 "provenance":"test"
1801 }"#;
1802 let result = serde_json::from_str::<WireProposedFact>(json);
1803 assert!(result.is_err());
1804 }
1805
1806 #[test]
1807 fn proposed_fact_wire_round_trips_through_registry() {
1808 let payload = TestPayload {
1809 kind: "vote".into(),
1810 score: 0.7,
1811 };
1812 let pf = ProposedFact::new(
1813 ContextKey::Hypotheses,
1814 "p",
1815 payload.clone(),
1816 TestProvenance.provenance(),
1817 );
1818 let wire = pf.to_wire().unwrap();
1819 let mut registry = PayloadRegistry::new();
1820 registry.register::<TestPayload>();
1821
1822 let decoded = ProposedFact::from_wire(wire, ®istry).unwrap();
1823
1824 assert_eq!(decoded.key, ContextKey::Hypotheses);
1825 assert_eq!(decoded.id, "p");
1826 assert_eq!(decoded.provenance(), "test-provenance");
1827 assert_eq!(decoded.require_payload::<TestPayload>().unwrap(), &payload);
1828 }
1829
1830 #[test]
1831 fn proposed_fact_from_wire_fails_closed_for_unknown_family_version() {
1832 let wire = WireProposedFact {
1833 key: ContextKey::Hypotheses,
1834 id: "p".into(),
1835 payload: WireFactPayload {
1836 family: FactFamilyId::new("unknown.payload"),
1837 version: PayloadVersion::new(1),
1838 payload: serde_json::json!({"kind":"vote"}),
1839 },
1840 confidence: UnitInterval::ONE,
1841 provenance: TestProvenance.provenance(),
1842 };
1843
1844 let registry = PayloadRegistry::new();
1845 let result = ProposedFact::from_wire(wire, ®istry);
1846
1847 assert!(matches!(
1848 result,
1849 Err(PayloadError::UnknownFamilyVersion { .. })
1850 ));
1851 }
1852
1853 #[test]
1854 fn context_fact_wire_round_trips_through_registry() {
1855 let payload = TestPayload {
1856 kind: "fact".into(),
1857 score: 0.9,
1858 };
1859 let fact = ContextFact::new_projection(
1860 ContextKey::Seeds,
1861 "f",
1862 payload.clone(),
1863 projection_record(),
1864 Timestamp::epoch(),
1865 );
1866 let wire = fact.to_wire().unwrap();
1867 let mut registry = PayloadRegistry::new();
1868 registry.register::<TestPayload>();
1869
1870 let decoded = ContextFact::from_wire(wire, ®istry).unwrap();
1871
1872 assert_eq!(decoded.key(), ContextKey::Seeds);
1873 assert_eq!(decoded.id(), "f");
1874 assert_eq!(decoded.require_payload::<TestPayload>().unwrap(), &payload);
1875 }
1876
1877 #[test]
1878 fn proposed_fact_to_context_fact_preserves_typed_payload() {
1879 let payload = TestPayload {
1880 kind: "proposal".into(),
1881 score: 0.8,
1882 };
1883 let proposal = ProposedFact::new(
1884 ContextKey::Strategies,
1885 "p",
1886 payload.clone(),
1887 TestProvenance.provenance(),
1888 );
1889
1890 let fact = proposal.to_context_fact("f", projection_record(), Timestamp::epoch());
1891
1892 assert_eq!(fact.key(), ContextKey::Strategies);
1893 assert_eq!(fact.require_payload::<TestPayload>().unwrap(), &payload);
1894 }
1895
1896 #[test]
1897 fn validation_error_display() {
1898 let err = ValidationError {
1899 reason: "bad input".into(),
1900 };
1901 assert_eq!(err.to_string(), "validation failed: bad input");
1902 }
1903
1904 #[test]
1905 fn validation_error_is_std_error() {
1906 let err = ValidationError {
1907 reason: "test".into(),
1908 };
1909 let _: &dyn std::error::Error = &err;
1910 }
1911
1912 #[test]
1913 fn fact_accessors() {
1914 let fact = projection_fact(ContextKey::Constraints, "f2", "body");
1915 assert_eq!(fact.key(), ContextKey::Constraints);
1916 assert_eq!(fact.id(), "f2");
1917 assert_eq!(fact.text(), Some("body"));
1918 assert_eq!(fact.created_at(), "1970-01-01T00:00:00Z");
1919 assert_eq!(fact.promotion_record().gate_id(), "projection-test");
1920 }
1921
1922 #[test]
1923 fn fact_actor_accessors() {
1924 let actor = FactActor::new_projection("agent-x", FactActorKind::Suggestor);
1925 assert_eq!(actor.id(), "agent-x");
1926 assert_eq!(actor.kind(), FactActorKind::Suggestor);
1927 }
1928
1929 #[test]
1930 fn validation_summary_accessors() {
1931 let vs = FactValidationSummary::new_projection(
1932 vec!["check-a".into()],
1933 vec!["check-b".into()],
1934 vec!["warn-c".into()],
1935 );
1936 assert_eq!(vs.checks_passed(), &["check-a"]);
1937 assert_eq!(vs.checks_skipped(), &["check-b"]);
1938 assert_eq!(vs.warnings(), &["warn-c"]);
1939 }
1940
1941 #[test]
1942 fn local_trace_accessors() {
1943 let lt =
1944 FactLocalTrace::new_projection("trace-1", "span-1", Some("parent-1".into()), false);
1945 assert_eq!(lt.trace_id(), "trace-1");
1946 assert_eq!(lt.span_id(), "span-1");
1947 assert_eq!(lt.parent_span_id().map(SpanId::as_str), Some("parent-1"));
1948 assert!(!lt.sampled());
1949 }
1950
1951 #[test]
1952 fn remote_trace_accessors() {
1953 let rt =
1954 FactRemoteTrace::new_projection("sys", "ref", Some("auth".into()), Some("30d".into()));
1955 assert_eq!(rt.system(), "sys");
1956 assert_eq!(rt.reference(), "ref");
1957 assert_eq!(rt.retrieval_auth(), Some("auth"));
1958 assert_eq!(rt.retention_hint(), Some("30d"));
1959 }
1960
1961 mod prop {
1962 use super::*;
1963 use proptest::prelude::*;
1964
1965 fn arb_context_key() -> impl Strategy<Value = ContextKey> {
1966 prop_oneof![
1967 Just(ContextKey::Seeds),
1968 Just(ContextKey::Hypotheses),
1969 Just(ContextKey::Strategies),
1970 Just(ContextKey::Constraints),
1971 Just(ContextKey::Signals),
1972 Just(ContextKey::Competitors),
1973 Just(ContextKey::Evaluations),
1974 Just(ContextKey::Proposals),
1975 Just(ContextKey::Diagnostic),
1976 Just(ContextKey::Votes),
1977 Just(ContextKey::Disagreements),
1978 Just(ContextKey::ConsensusOutcomes),
1979 ]
1980 }
1981
1982 proptest! {
1983 #[test]
1984 fn proposed_fact_always_constructible(
1985 key in arb_context_key(),
1986 id in "[a-z]{1,20}",
1987 content in ".*",
1988 prov in "[a-z0-9-]{1,30}",
1989 ) {
1990 let pf = ProposedFact::new(
1991 key,
1992 id.clone(),
1993 TextPayload::new(content.clone()),
1994 Provenance::new(prov.clone()),
1995 );
1996 prop_assert_eq!(pf.key, key);
1997 prop_assert_eq!(&pf.id, &id);
1998 prop_assert_eq!(pf.text(), Some(content.as_str()));
1999 prop_assert_eq!(pf.provenance(), prov.as_str());
2000 prop_assert!((pf.confidence() - 1.0).abs() < f64::EPSILON);
2001 }
2002 }
2003 }
2004}