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<&'static str> for Provenance {
112 fn from(value: &'static 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 {
159 fn as_str(&self) -> &'static str;
163
164 #[must_use]
167 fn proposed_fact<T>(
168 self,
169 key: ContextKey,
170 id: impl Into<ProposalId>,
171 payload: T,
172 ) -> ProposedFact
173 where
174 T: FactPayload + PartialEq,
175 {
176 ProposedFact::new(key, id, payload, self.as_str())
177 }
178}
179
180pub trait FactPayload: fmt::Debug + Clone + Serialize + Send + Sync + 'static {
185 const FAMILY: &'static str;
187 const VERSION: u16;
189
190 fn validate(&self) -> Result<(), PayloadError> {
193 Ok(())
194 }
195}
196
197trait ErasedFactPayload: fmt::Debug + Send + Sync {
198 fn family(&self) -> FactFamilyId;
199 fn version(&self) -> PayloadVersion;
200 fn validate(&self) -> Result<(), PayloadError>;
201 fn as_any(&self) -> &dyn Any;
202 fn to_json_value(&self) -> Result<serde_json::Value, PayloadError>;
203 fn equivalent(&self, other: &dyn ErasedFactPayload) -> bool;
204}
205
206impl<T> ErasedFactPayload for T
207where
208 T: FactPayload + PartialEq,
209{
210 fn family(&self) -> FactFamilyId {
211 FactFamilyId::from(T::FAMILY)
212 }
213
214 fn version(&self) -> PayloadVersion {
215 PayloadVersion::new(T::VERSION)
216 }
217
218 fn validate(&self) -> Result<(), PayloadError> {
219 FactPayload::validate(self)
220 }
221
222 fn as_any(&self) -> &dyn Any {
223 self
224 }
225
226 fn to_json_value(&self) -> Result<serde_json::Value, PayloadError> {
227 serde_json::to_value(self).map_err(|err| PayloadError::Serialize {
228 family: T::FAMILY.into(),
229 version: T::VERSION.into(),
230 reason: err.to_string(),
231 })
232 }
233
234 fn equivalent(&self, other: &dyn ErasedFactPayload) -> bool {
235 other.as_any().downcast_ref::<T>() == Some(self)
236 }
237}
238
239#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
242#[serde(deny_unknown_fields)]
243pub struct TextPayload {
244 text: String,
245}
246
247impl TextPayload {
248 #[must_use]
250 pub fn new(text: impl Into<String>) -> Self {
251 Self { text: text.into() }
252 }
253
254 #[must_use]
256 pub fn as_str(&self) -> &str {
257 &self.text
258 }
259}
260
261impl FactPayload for TextPayload {
262 const FAMILY: &'static str = "converge.text";
263 const VERSION: u16 = 1;
264}
265
266#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
268#[serde(deny_unknown_fields)]
269pub struct DiagnosticPayload {
270 source: String,
271 message: String,
272}
273
274impl DiagnosticPayload {
275 #[must_use]
277 pub fn new(source: impl Into<String>, message: impl Into<String>) -> Self {
278 Self {
279 source: source.into(),
280 message: message.into(),
281 }
282 }
283
284 #[must_use]
286 pub fn source(&self) -> &str {
287 &self.source
288 }
289
290 #[must_use]
292 pub fn message(&self) -> &str {
293 &self.message
294 }
295}
296
297impl FactPayload for DiagnosticPayload {
298 const FAMILY: &'static str = "converge.diagnostic";
299 const VERSION: u16 = 1;
300}
301
302#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
304#[serde(deny_unknown_fields)]
305pub struct ExecutionProducerIdentity {
306 pub name: String,
308 pub version: String,
310}
311
312impl ExecutionProducerIdentity {
313 #[must_use]
315 pub fn new(name: impl Into<String>, version: impl Into<String>) -> Self {
316 Self {
317 name: name.into(),
318 version: version.into(),
319 }
320 }
321
322 fn validate(&self) -> Result<(), String> {
323 validate_non_empty("producer.name", &self.name)?;
324 validate_non_empty("producer.version", &self.version)
325 }
326}
327
328#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
330#[serde(deny_unknown_fields)]
331pub struct NativeExecutionIdentity {
332 pub backend: String,
334 pub version: String,
336 pub source_url: String,
338 pub expected_commit: String,
340 pub actual_commit: String,
342 pub source_mode: String,
344}
345
346impl NativeExecutionIdentity {
347 #[must_use]
349 pub fn new(
350 backend: impl Into<String>,
351 version: impl Into<String>,
352 source_url: impl Into<String>,
353 expected_commit: impl Into<String>,
354 actual_commit: impl Into<String>,
355 source_mode: impl Into<String>,
356 ) -> Self {
357 Self {
358 backend: backend.into(),
359 version: version.into(),
360 source_url: source_url.into(),
361 expected_commit: expected_commit.into(),
362 actual_commit: actual_commit.into(),
363 source_mode: source_mode.into(),
364 }
365 }
366
367 fn validate(&self) -> Result<(), String> {
368 validate_non_empty("native_identity.backend", &self.backend)?;
369 validate_non_empty("native_identity.version", &self.version)?;
370 validate_non_empty("native_identity.source_url", &self.source_url)?;
371 validate_non_empty("native_identity.expected_commit", &self.expected_commit)?;
372 validate_non_empty("native_identity.actual_commit", &self.actual_commit)?;
373 validate_non_empty("native_identity.source_mode", &self.source_mode)
374 }
375}
376
377#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
384#[serde(deny_unknown_fields)]
385pub struct ExecutionIdentity {
386 pub producer: ExecutionProducerIdentity,
388 pub backend: String,
390 pub backend_version: String,
392 pub build_identity: String,
394 pub runtime_config: String,
396 pub native_identity: Option<NativeExecutionIdentity>,
398}
399
400impl ExecutionIdentity {
401 #[must_use]
403 pub fn new(
404 producer: ExecutionProducerIdentity,
405 backend: impl Into<String>,
406 backend_version: impl Into<String>,
407 build_identity: impl Into<String>,
408 runtime_config: impl Into<String>,
409 native_identity: Option<NativeExecutionIdentity>,
410 ) -> Self {
411 Self {
412 producer,
413 backend: backend.into(),
414 backend_version: backend_version.into(),
415 build_identity: build_identity.into(),
416 runtime_config: runtime_config.into(),
417 native_identity,
418 }
419 }
420
421 #[must_use]
423 pub fn non_native(
424 producer_name: impl Into<String>,
425 producer_version: impl Into<String>,
426 backend: impl Into<String>,
427 runtime_config: impl Into<String>,
428 ) -> Self {
429 Self::new(
430 ExecutionProducerIdentity::new(producer_name, producer_version),
431 backend,
432 "not_applicable",
433 "not_applicable",
434 runtime_config,
435 None,
436 )
437 }
438
439 #[must_use]
441 pub fn unspecified(
442 producer_name: impl Into<String>,
443 producer_version: impl Into<String>,
444 ) -> Self {
445 Self::new(
446 ExecutionProducerIdentity::new(producer_name, producer_version),
447 "unknown",
448 "unknown",
449 "unknown",
450 "unknown",
451 None,
452 )
453 }
454
455 #[must_use]
473 pub fn runtime_config_from_typed<T: Serialize>(value: &T) -> String {
474 serde_json::to_string(value)
475 .expect("typed runtime_config must serialize to JSON; check Serialize impl")
476 }
477
478 #[must_use]
482 pub fn with_runtime_config_typed<T: Serialize>(mut self, value: &T) -> Self {
483 self.runtime_config = Self::runtime_config_from_typed(value);
484 self
485 }
486
487 fn validate(&self) -> Result<(), String> {
488 self.producer.validate()?;
489 validate_non_empty("backend", &self.backend)?;
490 validate_non_empty("backend_version", &self.backend_version)?;
491 validate_non_empty("build_identity", &self.build_identity)?;
492 validate_non_empty("runtime_config", &self.runtime_config)?;
493 if let Some(native_identity) = &self.native_identity {
494 native_identity.validate()?;
495 }
496 Ok(())
497 }
498}
499
500#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
502#[serde(deny_unknown_fields)]
503pub struct ExecutionIdentityEvidence {
504 pub subject_key: ContextKey,
506 pub subject_id: String,
508 pub subject_family: FactFamilyId,
510 pub subject_version: PayloadVersion,
512 pub identity: ExecutionIdentity,
514}
515
516impl ExecutionIdentityEvidence {
517 #[must_use]
519 pub fn new(
520 subject_key: ContextKey,
521 subject_id: impl Into<String>,
522 subject_family: impl Into<FactFamilyId>,
523 subject_version: impl Into<PayloadVersion>,
524 identity: ExecutionIdentity,
525 ) -> Self {
526 Self {
527 subject_key,
528 subject_id: subject_id.into(),
529 subject_family: subject_family.into(),
530 subject_version: subject_version.into(),
531 identity,
532 }
533 }
534
535 #[must_use]
537 pub fn for_payload<T: FactPayload>(
538 subject_key: ContextKey,
539 subject_id: impl Into<String>,
540 identity: ExecutionIdentity,
541 ) -> Self {
542 Self::new(subject_key, subject_id, T::FAMILY, T::VERSION, identity)
543 }
544}
545
546impl FactPayload for ExecutionIdentityEvidence {
547 const FAMILY: &'static str = "converge.execution_identity.evidence";
548 const VERSION: u16 = 1;
549
550 fn validate(&self) -> Result<(), PayloadError> {
551 validate_non_empty("subject_id", &self.subject_id).map_err(|reason| {
552 PayloadError::Invalid {
553 family: Self::FAMILY.into(),
554 version: Self::VERSION.into(),
555 reason,
556 }
557 })?;
558 self.identity
559 .validate()
560 .map_err(|reason| PayloadError::Invalid {
561 family: Self::FAMILY.into(),
562 version: Self::VERSION.into(),
563 reason,
564 })
565 }
566}
567
568fn validate_non_empty(field: &str, value: &str) -> Result<(), String> {
569 if value.trim().is_empty() {
570 Err(format!("{field} must not be empty"))
571 } else {
572 Ok(())
573 }
574}
575
576#[derive(Debug, Clone, PartialEq, Eq, Error)]
578pub enum PayloadError {
579 #[error("invalid payload for {family} v{version}: {reason}")]
581 Invalid {
582 family: FactFamilyId,
584 version: PayloadVersion,
586 reason: String,
588 },
589 #[error("failed to serialize payload {family} v{version}: {reason}")]
591 Serialize {
592 family: FactFamilyId,
594 version: PayloadVersion,
596 reason: String,
598 },
599 #[error("failed to deserialize payload {family} v{version}: {reason}")]
601 Deserialize {
602 family: FactFamilyId,
604 version: PayloadVersion,
606 reason: String,
608 },
609 #[error("unknown payload family/version: {family} v{version}")]
611 UnknownFamilyVersion {
612 family: FactFamilyId,
614 version: PayloadVersion,
616 },
617 #[error(
619 "payload type mismatch: expected {expected} v{expected_version}, got {actual} v{actual_version}"
620 )]
621 TypeMismatch {
622 expected: FactFamilyId,
624 expected_version: PayloadVersion,
626 actual: FactFamilyId,
628 actual_version: PayloadVersion,
630 },
631}
632
633#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
636#[serde(deny_unknown_fields)]
637pub struct WireFactPayload {
638 pub family: FactFamilyId,
640 pub version: PayloadVersion,
642 pub payload: serde_json::Value,
644}
645
646impl WireFactPayload {
647 fn from_erased(payload: &dyn ErasedFactPayload) -> Result<Self, PayloadError> {
648 Ok(Self {
649 family: payload.family(),
650 version: payload.version(),
651 payload: payload.to_json_value()?,
652 })
653 }
654}
655
656#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
659#[serde(deny_unknown_fields)]
660pub struct WireProposedFact {
661 pub key: ContextKey,
663 pub id: ProposalId,
665 pub payload: WireFactPayload,
667 pub confidence: UnitInterval,
669 pub provenance: Provenance,
671}
672
673#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
675#[serde(deny_unknown_fields)]
676pub struct WireContextFact {
677 pub key: ContextKey,
679 pub id: FactId,
681 pub payload: WireFactPayload,
683 pub promotion_record: FactPromotionRecord,
685 pub created_at: Timestamp,
687}
688
689type PayloadDecoder = Box<
690 dyn Fn(serde_json::Value) -> Result<Arc<dyn ErasedFactPayload>, PayloadError> + Send + Sync,
691>;
692
693#[derive(Default)]
696pub struct PayloadRegistry {
697 decoders: HashMap<(FactFamilyId, PayloadVersion), PayloadDecoder>,
698}
699
700impl PayloadRegistry {
701 #[must_use]
703 pub fn new() -> Self {
704 Self::default()
705 }
706
707 pub fn register<T>(&mut self)
709 where
710 T: FactPayload + PartialEq + DeserializeOwned,
711 {
712 self.decoders.insert(
713 (
714 FactFamilyId::from(T::FAMILY),
715 PayloadVersion::new(T::VERSION),
716 ),
717 Box::new(|value| {
718 let payload: T =
719 serde_json::from_value(value).map_err(|err| PayloadError::Deserialize {
720 family: T::FAMILY.into(),
721 version: T::VERSION.into(),
722 reason: err.to_string(),
723 })?;
724 payload.validate()?;
725 Ok(Arc::new(payload))
726 }),
727 );
728 }
729
730 fn decode(
731 &self,
732 family: &FactFamilyId,
733 version: PayloadVersion,
734 payload: serde_json::Value,
735 ) -> Result<Arc<dyn ErasedFactPayload>, PayloadError> {
736 let decoder = self
737 .decoders
738 .get(&(family.clone(), version))
739 .ok_or_else(|| PayloadError::UnknownFamilyVersion {
740 family: family.clone(),
741 version,
742 })?;
743 decoder(payload)
744 }
745}
746
747#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
749pub enum FactActorKind {
750 Human,
752 Suggestor,
754 System,
756}
757
758#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
760pub struct FactActor {
761 id: ActorId,
762 kind: FactActorKind,
763}
764
765impl FactActor {
766 #[must_use]
768 pub fn id(&self) -> &ActorId {
769 &self.id
770 }
771
772 #[must_use]
774 pub fn kind(&self) -> FactActorKind {
775 self.kind
776 }
777
778 #[doc(hidden)]
779 pub fn new_projection(id: impl Into<ActorId>, kind: FactActorKind) -> Self {
780 Self {
781 id: id.into(),
782 kind,
783 }
784 }
785}
786
787#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize, Default)]
789pub struct FactValidationSummary {
790 checks_passed: Vec<ValidationCheckId>,
791 checks_skipped: Vec<ValidationCheckId>,
792 warnings: Vec<String>,
793}
794
795impl FactValidationSummary {
796 #[must_use]
798 pub fn checks_passed(&self) -> &[ValidationCheckId] {
799 &self.checks_passed
800 }
801
802 #[must_use]
804 pub fn checks_skipped(&self) -> &[ValidationCheckId] {
805 &self.checks_skipped
806 }
807
808 #[must_use]
810 pub fn warnings(&self) -> &[String] {
811 &self.warnings
812 }
813
814 #[doc(hidden)]
815 pub fn new_projection(
816 checks_passed: Vec<ValidationCheckId>,
817 checks_skipped: Vec<ValidationCheckId>,
818 warnings: Vec<String>,
819 ) -> Self {
820 Self {
821 checks_passed,
822 checks_skipped,
823 warnings,
824 }
825 }
826}
827
828#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
830#[serde(tag = "type", content = "id")]
831pub enum FactEvidenceRef {
832 Observation(ObservationId),
834 HumanApproval(ApprovalId),
836 Derived(ArtifactId),
838}
839
840#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
842pub struct FactLocalTrace {
843 trace_id: TraceId,
844 span_id: SpanId,
845 parent_span_id: Option<SpanId>,
846 sampled: bool,
847}
848
849impl FactLocalTrace {
850 #[must_use]
852 pub fn trace_id(&self) -> &TraceId {
853 &self.trace_id
854 }
855
856 #[must_use]
858 pub fn span_id(&self) -> &SpanId {
859 &self.span_id
860 }
861
862 #[must_use]
864 pub fn parent_span_id(&self) -> Option<&SpanId> {
865 self.parent_span_id.as_ref()
866 }
867
868 #[must_use]
870 pub fn sampled(&self) -> bool {
871 self.sampled
872 }
873
874 #[doc(hidden)]
875 pub fn new_projection(
876 trace_id: impl Into<TraceId>,
877 span_id: impl Into<SpanId>,
878 parent_span_id: Option<SpanId>,
879 sampled: bool,
880 ) -> Self {
881 Self {
882 trace_id: trace_id.into(),
883 span_id: span_id.into(),
884 parent_span_id,
885 sampled,
886 }
887 }
888}
889
890#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
892pub struct FactRemoteTrace {
893 system: TraceSystemId,
894 reference: TraceReference,
895 retrieval_auth: Option<String>,
896 retention_hint: Option<String>,
897}
898
899impl FactRemoteTrace {
900 #[must_use]
902 pub fn system(&self) -> &TraceSystemId {
903 &self.system
904 }
905
906 #[must_use]
908 pub fn reference(&self) -> &TraceReference {
909 &self.reference
910 }
911
912 #[must_use]
914 pub fn retrieval_auth(&self) -> Option<&str> {
915 self.retrieval_auth.as_deref()
916 }
917
918 #[must_use]
920 pub fn retention_hint(&self) -> Option<&str> {
921 self.retention_hint.as_deref()
922 }
923
924 #[doc(hidden)]
925 pub fn new_projection(
926 system: impl Into<TraceSystemId>,
927 reference: impl Into<TraceReference>,
928 retrieval_auth: Option<String>,
929 retention_hint: Option<String>,
930 ) -> Self {
931 Self {
932 system: system.into(),
933 reference: reference.into(),
934 retrieval_auth,
935 retention_hint,
936 }
937 }
938}
939
940#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
942#[serde(tag = "type")]
943pub enum FactTraceLink {
944 Local(FactLocalTrace),
946 Remote(FactRemoteTrace),
948}
949
950impl FactTraceLink {
951 #[must_use]
953 pub fn is_replay_eligible(&self) -> bool {
954 matches!(self, Self::Local(_))
955 }
956}
957
958#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
960pub struct FactPromotionRecord {
961 gate_id: GateId,
962 policy_version_hash: ContentHash,
963 approver: FactActor,
964 validation_summary: FactValidationSummary,
965 evidence_refs: Vec<FactEvidenceRef>,
966 trace_link: FactTraceLink,
967 promoted_at: Timestamp,
968}
969
970impl FactPromotionRecord {
971 #[must_use]
973 pub fn gate_id(&self) -> &GateId {
974 &self.gate_id
975 }
976
977 #[must_use]
979 pub fn policy_version_hash(&self) -> &ContentHash {
980 &self.policy_version_hash
981 }
982
983 #[must_use]
985 pub fn approver(&self) -> &FactActor {
986 &self.approver
987 }
988
989 #[must_use]
991 pub fn validation_summary(&self) -> &FactValidationSummary {
992 &self.validation_summary
993 }
994
995 #[must_use]
997 pub fn evidence_refs(&self) -> &[FactEvidenceRef] {
998 &self.evidence_refs
999 }
1000
1001 #[must_use]
1003 pub fn trace_link(&self) -> &FactTraceLink {
1004 &self.trace_link
1005 }
1006
1007 #[must_use]
1009 pub fn promoted_at(&self) -> &Timestamp {
1010 &self.promoted_at
1011 }
1012
1013 #[must_use]
1015 pub fn is_replay_eligible(&self) -> bool {
1016 self.trace_link.is_replay_eligible()
1017 }
1018
1019 #[doc(hidden)]
1020 pub fn new_projection(
1021 gate_id: impl Into<GateId>,
1022 policy_version_hash: ContentHash,
1023 approver: FactActor,
1024 validation_summary: FactValidationSummary,
1025 evidence_refs: Vec<FactEvidenceRef>,
1026 trace_link: FactTraceLink,
1027 promoted_at: impl Into<Timestamp>,
1028 ) -> Self {
1029 Self {
1030 gate_id: gate_id.into(),
1031 policy_version_hash,
1032 approver,
1033 validation_summary,
1034 evidence_refs,
1035 trace_link,
1036 promoted_at: promoted_at.into(),
1037 }
1038 }
1039}
1040
1041#[derive(Clone)]
1048pub struct ContextFact {
1049 key: ContextKey,
1051 id: FactId,
1053 payload: Arc<dyn ErasedFactPayload>,
1055 promotion_record: FactPromotionRecord,
1057 created_at: Timestamp,
1059}
1060
1061impl fmt::Debug for ContextFact {
1062 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
1063 f.debug_struct("ContextFact")
1064 .field("key", &self.key)
1065 .field("id", &self.id)
1066 .field("payload_family", &self.payload_family())
1067 .field("payload_version", &self.payload_version())
1068 .field("promotion_record", &self.promotion_record)
1069 .field("created_at", &self.created_at)
1070 .finish()
1071 }
1072}
1073
1074impl PartialEq for ContextFact {
1075 fn eq(&self, other: &Self) -> bool {
1076 self.key == other.key
1077 && self.id == other.id
1078 && self.payload_family() == other.payload_family()
1079 && self.payload_version() == other.payload_version()
1080 && self.payload.equivalent(other.payload.as_ref())
1081 && self.promotion_record == other.promotion_record
1082 && self.created_at == other.created_at
1083 }
1084}
1085
1086impl Eq for ContextFact {}
1087
1088impl Serialize for ContextFact {
1089 fn serialize<S>(&self, serializer: S) -> Result<S::Ok, S::Error>
1090 where
1091 S: serde::Serializer,
1092 {
1093 self.to_wire()
1094 .map_err(serde::ser::Error::custom)?
1095 .serialize(serializer)
1096 }
1097}
1098
1099impl ContextFact {
1100 #[must_use]
1106 pub fn new_projection<T>(
1107 key: ContextKey,
1108 id: impl Into<FactId>,
1109 payload: T,
1110 promotion_record: FactPromotionRecord,
1111 created_at: impl Into<Timestamp>,
1112 ) -> Self
1113 where
1114 T: FactPayload + PartialEq,
1115 {
1116 Self {
1117 key,
1118 id: id.into(),
1119 payload: Arc::new(payload),
1120 promotion_record,
1121 created_at: created_at.into(),
1122 }
1123 }
1124
1125 pub fn from_wire(
1128 wire: WireContextFact,
1129 registry: &PayloadRegistry,
1130 ) -> Result<Self, PayloadError> {
1131 let payload = registry.decode(
1132 &wire.payload.family,
1133 wire.payload.version,
1134 wire.payload.payload,
1135 )?;
1136 Ok(Self {
1137 key: wire.key,
1138 id: wire.id,
1139 payload,
1140 promotion_record: wire.promotion_record,
1141 created_at: wire.created_at,
1142 })
1143 }
1144
1145 pub fn to_wire(&self) -> Result<WireContextFact, PayloadError> {
1147 Ok(WireContextFact {
1148 key: self.key,
1149 id: self.id.clone(),
1150 payload: WireFactPayload::from_erased(self.payload.as_ref())?,
1151 promotion_record: self.promotion_record.clone(),
1152 created_at: self.created_at.clone(),
1153 })
1154 }
1155
1156 #[must_use]
1158 pub fn key(&self) -> ContextKey {
1159 self.key
1160 }
1161
1162 #[must_use]
1164 pub fn id(&self) -> &FactId {
1165 &self.id
1166 }
1167
1168 #[must_use]
1171 pub fn payload<T: FactPayload>(&self) -> Option<&T> {
1172 self.payload.as_any().downcast_ref::<T>()
1173 }
1174
1175 pub fn require_payload<T: FactPayload>(&self) -> Result<&T, PayloadError> {
1177 self.payload::<T>()
1178 .ok_or_else(|| PayloadError::TypeMismatch {
1179 expected: T::FAMILY.into(),
1180 expected_version: T::VERSION.into(),
1181 actual: self.payload_family(),
1182 actual_version: self.payload_version(),
1183 })
1184 }
1185
1186 #[must_use]
1188 pub fn payload_family(&self) -> FactFamilyId {
1189 self.payload.family()
1190 }
1191
1192 #[must_use]
1194 pub fn payload_version(&self) -> PayloadVersion {
1195 self.payload.version()
1196 }
1197
1198 #[must_use]
1200 pub fn text(&self) -> Option<&str> {
1201 self.payload::<TextPayload>().map(TextPayload::as_str)
1202 }
1203
1204 pub fn validate_payload(&self) -> Result<(), PayloadError> {
1206 self.payload.validate()
1207 }
1208
1209 #[must_use]
1211 pub fn promotion_record(&self) -> &FactPromotionRecord {
1212 &self.promotion_record
1213 }
1214
1215 #[must_use]
1217 pub fn created_at(&self) -> &Timestamp {
1218 &self.created_at
1219 }
1220
1221 #[must_use]
1223 pub fn is_replay_eligible(&self) -> bool {
1224 self.promotion_record.is_replay_eligible()
1225 }
1226}
1227
1228#[derive(Clone)]
1233pub struct ProposedFact {
1234 pub key: ContextKey,
1236 pub id: ProposalId,
1238 payload: Arc<dyn ErasedFactPayload>,
1240 confidence: UnitInterval,
1242 pub provenance: Provenance,
1244}
1245
1246impl fmt::Debug for ProposedFact {
1247 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
1248 f.debug_struct("ProposedFact")
1249 .field("key", &self.key)
1250 .field("id", &self.id)
1251 .field("payload_family", &self.payload_family())
1252 .field("payload_version", &self.payload_version())
1253 .field("confidence", &self.confidence)
1254 .field("provenance", &self.provenance)
1255 .finish()
1256 }
1257}
1258
1259impl PartialEq for ProposedFact {
1260 fn eq(&self, other: &Self) -> bool {
1261 self.key == other.key
1262 && self.id == other.id
1263 && self.payload_family() == other.payload_family()
1264 && self.payload_version() == other.payload_version()
1265 && self.payload.equivalent(other.payload.as_ref())
1266 && self.confidence == other.confidence
1267 && self.provenance == other.provenance
1268 }
1269}
1270
1271impl Serialize for ProposedFact {
1272 fn serialize<S>(&self, serializer: S) -> Result<S::Ok, S::Error>
1273 where
1274 S: serde::Serializer,
1275 {
1276 self.to_wire()
1277 .map_err(serde::ser::Error::custom)?
1278 .serialize(serializer)
1279 }
1280}
1281
1282impl ProposedFact {
1283 #[must_use]
1287 pub fn new<T>(
1288 key: ContextKey,
1289 id: impl Into<ProposalId>,
1290 payload: T,
1291 provenance: impl Into<Provenance>,
1292 ) -> Self
1293 where
1294 T: FactPayload + PartialEq,
1295 {
1296 Self {
1297 key,
1298 id: id.into(),
1299 payload: Arc::new(payload),
1300 confidence: UnitInterval::ONE,
1301 provenance: provenance.into(),
1302 }
1303 }
1304
1305 pub fn from_wire(
1308 wire: WireProposedFact,
1309 registry: &PayloadRegistry,
1310 ) -> Result<Self, PayloadError> {
1311 let payload = registry.decode(
1312 &wire.payload.family,
1313 wire.payload.version,
1314 wire.payload.payload,
1315 )?;
1316 Ok(Self {
1317 key: wire.key,
1318 id: wire.id,
1319 payload,
1320 confidence: wire.confidence,
1321 provenance: wire.provenance,
1322 })
1323 }
1324
1325 pub fn to_wire(&self) -> Result<WireProposedFact, PayloadError> {
1327 Ok(WireProposedFact {
1328 key: self.key,
1329 id: self.id.clone(),
1330 payload: WireFactPayload::from_erased(self.payload.as_ref())?,
1331 confidence: self.confidence,
1332 provenance: self.provenance.clone(),
1333 })
1334 }
1335
1336 #[must_use]
1343 pub fn to_context_fact(
1344 &self,
1345 id: impl Into<FactId>,
1346 promotion_record: FactPromotionRecord,
1347 created_at: impl Into<Timestamp>,
1348 ) -> ContextFact {
1349 ContextFact {
1350 key: self.key,
1351 id: id.into(),
1352 payload: Arc::clone(&self.payload),
1353 promotion_record,
1354 created_at: created_at.into(),
1355 }
1356 }
1357
1358 #[must_use]
1360 pub fn key(&self) -> ContextKey {
1361 self.key
1362 }
1363
1364 #[must_use]
1366 pub fn id(&self) -> &ProposalId {
1367 &self.id
1368 }
1369
1370 #[must_use]
1373 pub fn payload<T: FactPayload>(&self) -> Option<&T> {
1374 self.payload.as_any().downcast_ref::<T>()
1375 }
1376
1377 pub fn require_payload<T: FactPayload>(&self) -> Result<&T, PayloadError> {
1379 self.payload::<T>()
1380 .ok_or_else(|| PayloadError::TypeMismatch {
1381 expected: T::FAMILY.into(),
1382 expected_version: T::VERSION.into(),
1383 actual: self.payload_family(),
1384 actual_version: self.payload_version(),
1385 })
1386 }
1387
1388 #[must_use]
1390 pub fn payload_family(&self) -> FactFamilyId {
1391 self.payload.family()
1392 }
1393
1394 #[must_use]
1396 pub fn payload_version(&self) -> PayloadVersion {
1397 self.payload.version()
1398 }
1399
1400 #[must_use]
1402 pub fn text(&self) -> Option<&str> {
1403 self.payload::<TextPayload>().map(TextPayload::as_str)
1404 }
1405
1406 pub fn validate_payload(&self) -> Result<(), PayloadError> {
1408 self.payload.validate()
1409 }
1410
1411 #[must_use]
1413 pub fn provenance(&self) -> &str {
1414 self.provenance.as_str()
1415 }
1416
1417 #[must_use]
1419 pub fn confidence(&self) -> f64 {
1420 self.confidence.as_f64()
1421 }
1422
1423 #[must_use]
1431 pub fn with_confidence(mut self, confidence: f64) -> Self {
1432 self.confidence = UnitInterval::clamped(confidence);
1433 self
1434 }
1435
1436 #[must_use]
1454 pub fn adjust_confidence(mut self, delta: f64) -> Self {
1455 self.confidence = self.confidence.saturating_add(delta);
1456 self
1457 }
1458}
1459
1460pub const CONFIDENCE_STEP_TINY: f64 = 0.05;
1462
1463pub const CONFIDENCE_STEP_MINOR: f64 = 0.1;
1465
1466pub const CONFIDENCE_STEP_MEDIUM: f64 = 0.15;
1468
1469pub const CONFIDENCE_STEP_MAJOR: f64 = 0.2;
1471
1472pub const CONFIDENCE_STEP_PRIMARY: f64 = 0.25;
1474
1475#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
1477pub struct ValidationError {
1478 pub reason: String,
1480}
1481
1482impl std::fmt::Display for ValidationError {
1483 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
1484 write!(f, "validation failed: {}", self.reason)
1485 }
1486}
1487
1488impl std::error::Error for ValidationError {}
1489
1490#[cfg(test)]
1491mod tests {
1492 use super::*;
1493
1494 fn projection_record() -> FactPromotionRecord {
1495 FactPromotionRecord::new_projection(
1496 "projection-test",
1497 ContentHash::from_hex(
1498 "1111111111111111111111111111111111111111111111111111111111111111",
1499 ),
1500 FactActor::new_projection("actor-1", FactActorKind::System),
1501 FactValidationSummary::default(),
1502 Vec::new(),
1503 FactTraceLink::Local(FactLocalTrace::new_projection(
1504 "trace-1", "span-1", None, true,
1505 )),
1506 Timestamp::epoch(),
1507 )
1508 }
1509
1510 fn projection_fact(
1511 key: ContextKey,
1512 id: impl Into<FactId>,
1513 content: impl Into<String>,
1514 ) -> ContextFact {
1515 ContextFact::new_projection(
1516 key,
1517 id,
1518 TextPayload::new(content),
1519 projection_record(),
1520 Timestamp::epoch(),
1521 )
1522 }
1523
1524 #[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
1525 #[serde(deny_unknown_fields)]
1526 struct TestPayload {
1527 kind: String,
1528 score: f64,
1529 }
1530
1531 impl FactPayload for TestPayload {
1532 const FAMILY: &'static str = "test.payload";
1533 const VERSION: u16 = 1;
1534 }
1535
1536 fn native_identity() -> NativeExecutionIdentity {
1537 NativeExecutionIdentity::new(
1538 "CVC5",
1539 "1.3.3",
1540 "https://github.com/cvc5/cvc5",
1541 "expected",
1542 "actual",
1543 "vendored",
1544 )
1545 }
1546
1547 #[test]
1548 fn execution_identity_evidence_targets_typed_payload() {
1549 let identity = ExecutionIdentity::new(
1550 ExecutionProducerIdentity::new("soter", "0.1.0"),
1551 "cvc5",
1552 "1.3.3",
1553 "configure_flags=--no-poly",
1554 "timeout_ms=5000",
1555 Some(native_identity()),
1556 );
1557 let evidence = ExecutionIdentityEvidence::for_payload::<TestPayload>(
1558 ContextKey::Evaluations,
1559 "smt-report-q1",
1560 identity,
1561 );
1562
1563 assert_eq!(evidence.subject_key, ContextKey::Evaluations);
1564 assert_eq!(evidence.subject_id, "smt-report-q1");
1565 assert_eq!(evidence.subject_family, FactFamilyId::from("test.payload"));
1566 assert_eq!(evidence.subject_version, PayloadVersion::new(1));
1567 assert_eq!(evidence.identity.backend, "cvc5");
1568 assert!(FactPayload::validate(&evidence).is_ok());
1569 }
1570
1571 #[test]
1572 fn execution_identity_evidence_rejects_empty_subject_id() {
1573 let evidence = ExecutionIdentityEvidence::for_payload::<TestPayload>(
1574 ContextKey::Strategies,
1575 "",
1576 ExecutionIdentity::non_native("ferrox", "0.5.1", "greedy", "tasks=3"),
1577 );
1578
1579 assert!(matches!(
1580 FactPayload::validate(&evidence),
1581 Err(PayloadError::Invalid { .. })
1582 ));
1583 }
1584
1585 #[test]
1586 fn trace_link_local_is_replay_eligible() {
1587 let local = FactTraceLink::Local(FactLocalTrace {
1588 trace_id: "t1".into(),
1589 span_id: "s1".into(),
1590 parent_span_id: None,
1591 sampled: true,
1592 });
1593 assert!(local.is_replay_eligible());
1594 }
1595
1596 #[test]
1597 fn trace_link_remote_is_not_replay_eligible() {
1598 let remote = FactTraceLink::Remote(FactRemoteTrace {
1599 system: "datadog".into(),
1600 reference: "ref-1".into(),
1601 retrieval_auth: None,
1602 retention_hint: None,
1603 });
1604 assert!(!remote.is_replay_eligible());
1605 }
1606
1607 #[test]
1608 fn promotion_record_delegates_replay_eligibility() {
1609 let local_record = FactPromotionRecord::new_projection(
1610 "gate-1",
1611 ContentHash::from_hex(
1612 "1111111111111111111111111111111111111111111111111111111111111111",
1613 ),
1614 FactActor::new_projection("actor-1", FactActorKind::Human),
1615 FactValidationSummary::default(),
1616 Vec::new(),
1617 FactTraceLink::Local(FactLocalTrace::new_projection("t1", "s1", None, true)),
1618 "2026-01-01T00:00:00Z",
1619 );
1620 assert!(local_record.is_replay_eligible());
1621
1622 let remote_record = FactPromotionRecord::new_projection(
1623 "gate-2",
1624 ContentHash::from_hex(
1625 "2222222222222222222222222222222222222222222222222222222222222222",
1626 ),
1627 FactActor::new_projection("actor-2", FactActorKind::System),
1628 FactValidationSummary::default(),
1629 Vec::new(),
1630 FactTraceLink::Remote(FactRemoteTrace::new_projection("dd", "ref-1", None, None)),
1631 "2026-01-01T00:00:00Z",
1632 );
1633 assert!(!remote_record.is_replay_eligible());
1634 }
1635
1636 #[test]
1637 fn fact_delegates_replay_eligibility() {
1638 let fact = projection_fact(ContextKey::Seeds, "f1", "content");
1639 assert!(fact.is_replay_eligible());
1640 }
1641
1642 #[test]
1643 fn proposed_fact_new_sets_fields() {
1644 let pf = ProposedFact::new(
1645 ContextKey::Hypotheses,
1646 "p1",
1647 TextPayload::new("my content"),
1648 "gpt-4",
1649 );
1650 assert_eq!(pf.key, ContextKey::Hypotheses);
1651 assert_eq!(pf.id, "p1");
1652 assert_eq!(pf.text(), Some("my content"));
1653 assert_eq!(pf.confidence(), 1.0);
1654 assert_eq!(pf.provenance(), "gpt-4");
1655 }
1656
1657 #[test]
1658 fn proposed_fact_with_confidence() {
1659 let pf = ProposedFact::new(ContextKey::Signals, "p2", TextPayload::new("c"), "prov")
1660 .with_confidence(0.42);
1661 assert!((pf.confidence() - 0.42).abs() < f64::EPSILON);
1662 }
1663
1664 #[test]
1665 fn adjust_confidence_accumulates() {
1666 let pf = ProposedFact::new(ContextKey::Seeds, "p", TextPayload::new("c"), "x")
1667 .with_confidence(0.5)
1668 .adjust_confidence(CONFIDENCE_STEP_MINOR)
1669 .adjust_confidence(CONFIDENCE_STEP_MAJOR);
1670 assert!((pf.confidence() - 0.8).abs() < f64::EPSILON);
1671 }
1672
1673 #[test]
1674 fn adjust_confidence_clamps_at_one() {
1675 let pf = ProposedFact::new(ContextKey::Seeds, "p", TextPayload::new("c"), "x")
1676 .with_confidence(0.9)
1677 .adjust_confidence(CONFIDENCE_STEP_MAJOR);
1678 assert_eq!(pf.confidence(), 1.0);
1679 }
1680
1681 #[test]
1682 fn adjust_confidence_clamps_at_zero() {
1683 let pf = ProposedFact::new(ContextKey::Seeds, "p", TextPayload::new("c"), "x")
1684 .with_confidence(0.1)
1685 .adjust_confidence(-0.5);
1686 assert_eq!(pf.confidence(), 0.0);
1687 }
1688
1689 #[test]
1690 fn with_confidence_clamps_high() {
1691 let pf = ProposedFact::new(ContextKey::Seeds, "p", TextPayload::new("c"), "x")
1692 .with_confidence(1.5);
1693 assert_eq!(pf.confidence(), 1.0);
1694 }
1695
1696 #[test]
1697 fn with_confidence_clamps_negative() {
1698 let pf = ProposedFact::new(ContextKey::Seeds, "p", TextPayload::new("c"), "x")
1699 .with_confidence(-0.1);
1700 assert_eq!(pf.confidence(), 0.0);
1701 }
1702
1703 #[test]
1704 fn with_confidence_normalizes_nan() {
1705 let pf = ProposedFact::new(ContextKey::Seeds, "p", TextPayload::new("c"), "x")
1706 .with_confidence(f64::NAN);
1707 assert_eq!(pf.confidence(), 0.0);
1708 }
1709
1710 #[test]
1711 fn with_confidence_normalizes_infinity() {
1712 let pf = ProposedFact::new(ContextKey::Seeds, "p", TextPayload::new("c"), "x")
1713 .with_confidence(f64::INFINITY);
1714 assert_eq!(pf.confidence(), 0.0);
1715 }
1716
1717 #[test]
1718 fn wire_proposed_fact_deserialization_rejects_out_of_range_confidence() {
1719 let json = r#"{
1720 "key":"Seeds",
1721 "id":"p",
1722 "payload":{
1723 "family":"converge.text",
1724 "version":1,
1725 "payload":{"text":"c"}
1726 },
1727 "confidence":1.5,
1728 "provenance":"test"
1729 }"#;
1730 let result = serde_json::from_str::<WireProposedFact>(json);
1731 assert!(result.is_err());
1732 }
1733
1734 #[test]
1735 fn proposed_fact_wire_round_trips_through_registry() {
1736 let payload = TestPayload {
1737 kind: "vote".into(),
1738 score: 0.7,
1739 };
1740 let pf = ProposedFact::new(ContextKey::Hypotheses, "p", payload.clone(), "test");
1741 let wire = pf.to_wire().unwrap();
1742 let mut registry = PayloadRegistry::new();
1743 registry.register::<TestPayload>();
1744
1745 let decoded = ProposedFact::from_wire(wire, ®istry).unwrap();
1746
1747 assert_eq!(decoded.key, ContextKey::Hypotheses);
1748 assert_eq!(decoded.id, "p");
1749 assert_eq!(decoded.provenance(), "test");
1750 assert_eq!(decoded.require_payload::<TestPayload>().unwrap(), &payload);
1751 }
1752
1753 #[test]
1754 fn proposed_fact_from_wire_fails_closed_for_unknown_family_version() {
1755 let wire = WireProposedFact {
1756 key: ContextKey::Hypotheses,
1757 id: "p".into(),
1758 payload: WireFactPayload {
1759 family: FactFamilyId::new("unknown.payload"),
1760 version: PayloadVersion::new(1),
1761 payload: serde_json::json!({"kind":"vote"}),
1762 },
1763 confidence: UnitInterval::ONE,
1764 provenance: Provenance::new("test"),
1765 };
1766
1767 let registry = PayloadRegistry::new();
1768 let result = ProposedFact::from_wire(wire, ®istry);
1769
1770 assert!(matches!(
1771 result,
1772 Err(PayloadError::UnknownFamilyVersion { .. })
1773 ));
1774 }
1775
1776 #[test]
1777 fn context_fact_wire_round_trips_through_registry() {
1778 let payload = TestPayload {
1779 kind: "fact".into(),
1780 score: 0.9,
1781 };
1782 let fact = ContextFact::new_projection(
1783 ContextKey::Seeds,
1784 "f",
1785 payload.clone(),
1786 projection_record(),
1787 Timestamp::epoch(),
1788 );
1789 let wire = fact.to_wire().unwrap();
1790 let mut registry = PayloadRegistry::new();
1791 registry.register::<TestPayload>();
1792
1793 let decoded = ContextFact::from_wire(wire, ®istry).unwrap();
1794
1795 assert_eq!(decoded.key(), ContextKey::Seeds);
1796 assert_eq!(decoded.id(), "f");
1797 assert_eq!(decoded.require_payload::<TestPayload>().unwrap(), &payload);
1798 }
1799
1800 #[test]
1801 fn proposed_fact_to_context_fact_preserves_typed_payload() {
1802 let payload = TestPayload {
1803 kind: "proposal".into(),
1804 score: 0.8,
1805 };
1806 let proposal = ProposedFact::new(ContextKey::Strategies, "p", payload.clone(), "test");
1807
1808 let fact = proposal.to_context_fact("f", projection_record(), Timestamp::epoch());
1809
1810 assert_eq!(fact.key(), ContextKey::Strategies);
1811 assert_eq!(fact.require_payload::<TestPayload>().unwrap(), &payload);
1812 }
1813
1814 #[test]
1815 fn validation_error_display() {
1816 let err = ValidationError {
1817 reason: "bad input".into(),
1818 };
1819 assert_eq!(err.to_string(), "validation failed: bad input");
1820 }
1821
1822 #[test]
1823 fn validation_error_is_std_error() {
1824 let err = ValidationError {
1825 reason: "test".into(),
1826 };
1827 let _: &dyn std::error::Error = &err;
1828 }
1829
1830 #[test]
1831 fn fact_accessors() {
1832 let fact = projection_fact(ContextKey::Constraints, "f2", "body");
1833 assert_eq!(fact.key(), ContextKey::Constraints);
1834 assert_eq!(fact.id(), "f2");
1835 assert_eq!(fact.text(), Some("body"));
1836 assert_eq!(fact.created_at(), "1970-01-01T00:00:00Z");
1837 assert_eq!(fact.promotion_record().gate_id(), "projection-test");
1838 }
1839
1840 #[test]
1841 fn fact_actor_accessors() {
1842 let actor = FactActor::new_projection("agent-x", FactActorKind::Suggestor);
1843 assert_eq!(actor.id(), "agent-x");
1844 assert_eq!(actor.kind(), FactActorKind::Suggestor);
1845 }
1846
1847 #[test]
1848 fn validation_summary_accessors() {
1849 let vs = FactValidationSummary::new_projection(
1850 vec!["check-a".into()],
1851 vec!["check-b".into()],
1852 vec!["warn-c".into()],
1853 );
1854 assert_eq!(vs.checks_passed(), &["check-a"]);
1855 assert_eq!(vs.checks_skipped(), &["check-b"]);
1856 assert_eq!(vs.warnings(), &["warn-c"]);
1857 }
1858
1859 #[test]
1860 fn local_trace_accessors() {
1861 let lt =
1862 FactLocalTrace::new_projection("trace-1", "span-1", Some("parent-1".into()), false);
1863 assert_eq!(lt.trace_id(), "trace-1");
1864 assert_eq!(lt.span_id(), "span-1");
1865 assert_eq!(lt.parent_span_id().map(SpanId::as_str), Some("parent-1"));
1866 assert!(!lt.sampled());
1867 }
1868
1869 #[test]
1870 fn remote_trace_accessors() {
1871 let rt =
1872 FactRemoteTrace::new_projection("sys", "ref", Some("auth".into()), Some("30d".into()));
1873 assert_eq!(rt.system(), "sys");
1874 assert_eq!(rt.reference(), "ref");
1875 assert_eq!(rt.retrieval_auth(), Some("auth"));
1876 assert_eq!(rt.retention_hint(), Some("30d"));
1877 }
1878
1879 mod prop {
1880 use super::*;
1881 use proptest::prelude::*;
1882
1883 fn arb_context_key() -> impl Strategy<Value = ContextKey> {
1884 prop_oneof![
1885 Just(ContextKey::Seeds),
1886 Just(ContextKey::Hypotheses),
1887 Just(ContextKey::Strategies),
1888 Just(ContextKey::Constraints),
1889 Just(ContextKey::Signals),
1890 Just(ContextKey::Competitors),
1891 Just(ContextKey::Evaluations),
1892 Just(ContextKey::Proposals),
1893 Just(ContextKey::Diagnostic),
1894 Just(ContextKey::Votes),
1895 Just(ContextKey::Disagreements),
1896 Just(ContextKey::ConsensusOutcomes),
1897 ]
1898 }
1899
1900 proptest! {
1901 #[test]
1902 fn proposed_fact_always_constructible(
1903 key in arb_context_key(),
1904 id in "[a-z]{1,20}",
1905 content in ".*",
1906 prov in "[a-z0-9-]{1,30}",
1907 ) {
1908 let pf = ProposedFact::new(key, id.clone(), TextPayload::new(content.clone()), prov.clone());
1909 prop_assert_eq!(pf.key, key);
1910 prop_assert_eq!(&pf.id, &id);
1911 prop_assert_eq!(pf.text(), Some(content.as_str()));
1912 prop_assert_eq!(pf.provenance(), prov.as_str());
1913 prop_assert!((pf.confidence() - 1.0).abs() < f64::EPSILON);
1914 }
1915 }
1916 }
1917}