Skip to main content

converge_pack/
fact.rs

1// Copyright 2024-2026 Reflective Labs
2// SPDX-License-Identifier: MIT
3
4//! Facts and proposed facts — the type boundary.
5//!
6//! This is the most important design decision in Converge: LLMs suggest,
7//! the engine validates. `ProposedFact` is not `Fact`. There is no implicit
8//! conversion between them.
9
10use serde::{Deserialize, Serialize};
11
12use crate::context::ContextKey;
13use crate::types::{
14    ActorId, ApprovalId, ArtifactId, ContentHash, FactId, GateId, ObservationId, ProposalId,
15    SpanId, Timestamp, TraceId, TraceReference, TraceSystemId, ValidationCheckId,
16};
17
18/// Actor kind recorded on a promoted fact.
19#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize)]
20pub enum FactActorKind {
21    /// Human approver.
22    Human,
23    /// Suggestor or automated domain actor.
24    Suggestor,
25    /// Kernel or system component.
26    System,
27}
28
29/// Read-only actor record attached to authoritative facts.
30#[derive(Debug, Clone, PartialEq, Eq, Serialize)]
31pub struct FactActor {
32    id: ActorId,
33    kind: FactActorKind,
34}
35
36impl FactActor {
37    /// Returns the actor identifier.
38    #[must_use]
39    pub fn id(&self) -> &ActorId {
40        &self.id
41    }
42
43    /// Returns the actor kind.
44    #[must_use]
45    pub fn kind(&self) -> FactActorKind {
46        self.kind
47    }
48
49    #[cfg(feature = "kernel-authority")]
50    #[doc(hidden)]
51    pub fn new(id: impl Into<ActorId>, kind: FactActorKind) -> Self {
52        Self {
53            id: id.into(),
54            kind,
55        }
56    }
57}
58
59/// Summary of validation checks attached to an authoritative fact.
60#[derive(Debug, Clone, PartialEq, Eq, Serialize, Default)]
61pub struct FactValidationSummary {
62    checks_passed: Vec<ValidationCheckId>,
63    checks_skipped: Vec<ValidationCheckId>,
64    warnings: Vec<String>,
65}
66
67impl FactValidationSummary {
68    /// Returns validation checks that passed.
69    #[must_use]
70    pub fn checks_passed(&self) -> &[ValidationCheckId] {
71        &self.checks_passed
72    }
73
74    /// Returns validation checks that were skipped.
75    #[must_use]
76    pub fn checks_skipped(&self) -> &[ValidationCheckId] {
77        &self.checks_skipped
78    }
79
80    /// Returns validation warnings.
81    #[must_use]
82    pub fn warnings(&self) -> &[String] {
83        &self.warnings
84    }
85
86    #[cfg(feature = "kernel-authority")]
87    #[doc(hidden)]
88    pub fn new(
89        checks_passed: Vec<ValidationCheckId>,
90        checks_skipped: Vec<ValidationCheckId>,
91        warnings: Vec<String>,
92    ) -> Self {
93        Self {
94            checks_passed,
95            checks_skipped,
96            warnings,
97        }
98    }
99}
100
101/// Typed evidence references attached to an authoritative fact.
102#[derive(Debug, Clone, PartialEq, Eq, Serialize)]
103#[serde(tag = "type", content = "id")]
104pub enum FactEvidenceRef {
105    /// Observation used as evidence.
106    Observation(ObservationId),
107    /// Human approval used as evidence.
108    HumanApproval(ApprovalId),
109    /// Derived artifact used as evidence.
110    Derived(ArtifactId),
111}
112
113/// Local replayable trace reference.
114#[derive(Debug, Clone, PartialEq, Eq, Serialize)]
115pub struct FactLocalTrace {
116    trace_id: TraceId,
117    span_id: SpanId,
118    parent_span_id: Option<SpanId>,
119    sampled: bool,
120}
121
122impl FactLocalTrace {
123    /// Returns the trace identifier.
124    #[must_use]
125    pub fn trace_id(&self) -> &TraceId {
126        &self.trace_id
127    }
128
129    /// Returns the span identifier.
130    #[must_use]
131    pub fn span_id(&self) -> &SpanId {
132        &self.span_id
133    }
134
135    /// Returns the parent span identifier.
136    #[must_use]
137    pub fn parent_span_id(&self) -> Option<&SpanId> {
138        self.parent_span_id.as_ref()
139    }
140
141    /// Returns whether the trace was sampled.
142    #[must_use]
143    pub fn sampled(&self) -> bool {
144        self.sampled
145    }
146
147    #[cfg(feature = "kernel-authority")]
148    #[doc(hidden)]
149    pub fn new(
150        trace_id: impl Into<TraceId>,
151        span_id: impl Into<SpanId>,
152        parent_span_id: Option<SpanId>,
153        sampled: bool,
154    ) -> Self {
155        Self {
156            trace_id: trace_id.into(),
157            span_id: span_id.into(),
158            parent_span_id,
159            sampled,
160        }
161    }
162}
163
164/// Remote audit-only trace reference.
165#[derive(Debug, Clone, PartialEq, Eq, Serialize)]
166pub struct FactRemoteTrace {
167    system: TraceSystemId,
168    reference: TraceReference,
169    retrieval_auth: Option<String>,
170    retention_hint: Option<String>,
171}
172
173impl FactRemoteTrace {
174    /// Returns the remote system identifier.
175    #[must_use]
176    pub fn system(&self) -> &TraceSystemId {
177        &self.system
178    }
179
180    /// Returns the remote trace reference.
181    #[must_use]
182    pub fn reference(&self) -> &TraceReference {
183        &self.reference
184    }
185
186    /// Returns the retrieval auth hint.
187    #[must_use]
188    pub fn retrieval_auth(&self) -> Option<&str> {
189        self.retrieval_auth.as_deref()
190    }
191
192    /// Returns the retention hint.
193    #[must_use]
194    pub fn retention_hint(&self) -> Option<&str> {
195        self.retention_hint.as_deref()
196    }
197
198    #[cfg(feature = "kernel-authority")]
199    #[doc(hidden)]
200    pub fn new(
201        system: impl Into<TraceSystemId>,
202        reference: impl Into<TraceReference>,
203        retrieval_auth: Option<String>,
204        retention_hint: Option<String>,
205    ) -> Self {
206        Self {
207            system: system.into(),
208            reference: reference.into(),
209            retrieval_auth,
210            retention_hint,
211        }
212    }
213}
214
215/// Trace record attached to an authoritative fact.
216#[derive(Debug, Clone, PartialEq, Eq, Serialize)]
217#[serde(tag = "type")]
218pub enum FactTraceLink {
219    /// Local replayable trace.
220    Local(FactLocalTrace),
221    /// Remote audit-only trace.
222    Remote(FactRemoteTrace),
223}
224
225impl FactTraceLink {
226    /// Returns whether the trace is replay-eligible.
227    #[must_use]
228    pub fn is_replay_eligible(&self) -> bool {
229        matches!(self, Self::Local(_))
230    }
231}
232
233/// Read-only promotion record attached to an authoritative fact.
234#[derive(Debug, Clone, PartialEq, Eq, Serialize)]
235pub struct FactPromotionRecord {
236    gate_id: GateId,
237    policy_version_hash: ContentHash,
238    approver: FactActor,
239    validation_summary: FactValidationSummary,
240    evidence_refs: Vec<FactEvidenceRef>,
241    trace_link: FactTraceLink,
242    promoted_at: Timestamp,
243}
244
245impl FactPromotionRecord {
246    /// Returns the gate identifier that promoted the fact.
247    #[must_use]
248    pub fn gate_id(&self) -> &GateId {
249        &self.gate_id
250    }
251
252    /// Returns the policy hash used during promotion.
253    #[must_use]
254    pub fn policy_version_hash(&self) -> &ContentHash {
255        &self.policy_version_hash
256    }
257
258    /// Returns the approving actor.
259    #[must_use]
260    pub fn approver(&self) -> &FactActor {
261        &self.approver
262    }
263
264    /// Returns the validation summary.
265    #[must_use]
266    pub fn validation_summary(&self) -> &FactValidationSummary {
267        &self.validation_summary
268    }
269
270    /// Returns the evidence references used during promotion.
271    #[must_use]
272    pub fn evidence_refs(&self) -> &[FactEvidenceRef] {
273        &self.evidence_refs
274    }
275
276    /// Returns the trace link for audit or replay.
277    #[must_use]
278    pub fn trace_link(&self) -> &FactTraceLink {
279        &self.trace_link
280    }
281
282    /// Returns the promotion timestamp.
283    #[must_use]
284    pub fn promoted_at(&self) -> &Timestamp {
285        &self.promoted_at
286    }
287
288    /// Returns whether the promotion is replay-eligible.
289    #[must_use]
290    pub fn is_replay_eligible(&self) -> bool {
291        self.trace_link.is_replay_eligible()
292    }
293
294    #[cfg(feature = "kernel-authority")]
295    #[doc(hidden)]
296    pub fn new(
297        gate_id: impl Into<GateId>,
298        policy_version_hash: ContentHash,
299        approver: FactActor,
300        validation_summary: FactValidationSummary,
301        evidence_refs: Vec<FactEvidenceRef>,
302        trace_link: FactTraceLink,
303        promoted_at: impl Into<Timestamp>,
304    ) -> Self {
305        Self {
306            gate_id: gate_id.into(),
307            policy_version_hash,
308            approver,
309            validation_summary,
310            evidence_refs,
311            trace_link,
312            promoted_at: promoted_at.into(),
313        }
314    }
315}
316
317/// A validated, authoritative assertion in the context.
318///
319/// Facts are append-only. Once added to the context, they are never
320/// mutated or removed (within a convergence run). History is preserved.
321#[derive(Debug, Clone, PartialEq, Eq, Serialize)]
322pub struct Fact {
323    /// Which context key this fact belongs to.
324    key: ContextKey,
325    /// Unique identifier within the context key namespace.
326    pub id: FactId,
327    /// The fact's content as a string. Interpretation is key-dependent.
328    pub content: String,
329    /// The immutable promotion record that made this fact authoritative.
330    promotion_record: FactPromotionRecord,
331    /// When the authoritative fact entered context.
332    created_at: Timestamp,
333}
334
335impl Fact {
336    /// Returns the context key this fact belongs to.
337    #[must_use]
338    pub fn key(&self) -> ContextKey {
339        self.key
340    }
341
342    /// Returns the immutable promotion record for this fact.
343    #[must_use]
344    pub fn promotion_record(&self) -> &FactPromotionRecord {
345        &self.promotion_record
346    }
347
348    /// Returns the fact creation timestamp.
349    #[must_use]
350    pub fn created_at(&self) -> &Timestamp {
351        &self.created_at
352    }
353
354    /// Returns whether the fact is replay-eligible.
355    #[must_use]
356    pub fn is_replay_eligible(&self) -> bool {
357        self.promotion_record.is_replay_eligible()
358    }
359}
360
361/// Kernel-only construction helpers for authoritative facts.
362#[cfg(feature = "kernel-authority")]
363#[doc(hidden)]
364pub mod kernel_authority {
365    use super::*;
366
367    /// Creates a kernel-authoritative fact with default promotion metadata.
368    #[must_use]
369    pub fn new_fact(key: ContextKey, id: impl Into<FactId>, content: impl Into<String>) -> Fact {
370        new_fact_with_promotion(
371            key,
372            id,
373            content,
374            FactPromotionRecord::new(
375                "kernel-authority",
376                ContentHash::zero(),
377                FactActor::new("converge-kernel", FactActorKind::System),
378                FactValidationSummary::default(),
379                Vec::new(),
380                FactTraceLink::Local(FactLocalTrace::new("kernel-authority", "seed", None, true)),
381                Timestamp::epoch(),
382            ),
383            Timestamp::epoch(),
384        )
385    }
386
387    /// Creates a kernel-authoritative fact with an explicit promotion record.
388    #[must_use]
389    pub fn new_fact_with_promotion(
390        key: ContextKey,
391        id: impl Into<FactId>,
392        content: impl Into<String>,
393        promotion_record: FactPromotionRecord,
394        created_at: impl Into<Timestamp>,
395    ) -> Fact {
396        Fact {
397            key,
398            id: id.into(),
399            content: content.into(),
400            promotion_record,
401            created_at: created_at.into(),
402        }
403    }
404}
405
406/// An unvalidated suggestion from a non-authoritative source.
407///
408/// Proposed facts live in `ContextKey::Proposals` until a `ValidationAgent`
409/// promotes them to `Fact`. The proposal tracks its origin for audit trail.
410#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
411pub struct ProposedFact {
412    /// The context key this proposal targets.
413    pub key: ContextKey,
414    /// Unique identifier encoding origin and target.
415    pub id: ProposalId,
416    /// The proposed content.
417    pub content: String,
418    /// Confidence hint from the source. Always in [0.0, 1.0].
419    confidence: f64,
420    /// Provenance information (e.g., model ID, prompt hash).
421    pub provenance: String,
422}
423
424impl ProposedFact {
425    /// Create a new draft proposal with explicit provenance.
426    ///
427    /// Confidence defaults to 1.0. Override with [`with_confidence`][Self::with_confidence].
428    #[must_use]
429    pub fn new(
430        key: ContextKey,
431        id: impl Into<ProposalId>,
432        content: impl Into<String>,
433        provenance: impl Into<String>,
434    ) -> Self {
435        Self {
436            key,
437            id: id.into(),
438            content: content.into(),
439            confidence: 1.0,
440            provenance: provenance.into(),
441        }
442    }
443
444    /// Returns the confidence value, always in [0.0, 1.0].
445    #[must_use]
446    pub fn confidence(&self) -> f64 {
447        self.confidence
448    }
449
450    /// Set an explicit confidence baseline for this proposal.
451    ///
452    /// Use this to establish a starting point, then accumulate criteria with
453    /// [`adjust_confidence`][Self::adjust_confidence]. The value is clamped to
454    /// [0.0, 1.0]; non-finite values (NaN, infinity) are treated as 0.0.
455    ///
456    /// For computed confidence (e.g. from a solver), pass the result directly.
457    #[must_use]
458    pub fn with_confidence(mut self, confidence: f64) -> Self {
459        self.confidence = if confidence.is_finite() {
460            confidence.clamp(0.0, 1.0)
461        } else {
462            0.0
463        };
464        self
465    }
466
467    /// Adjust confidence by a named step, clamped to [0.0, 1.0].
468    ///
469    /// This is the recommended way to express confidence in suggestors and pack
470    /// solvers. Use the `CONFIDENCE_STEP_*` constants as the vocabulary:
471    ///
472    /// ```rust,ignore
473    /// use converge_pack::{CONFIDENCE_STEP_MAJOR, CONFIDENCE_STEP_MINOR, CONFIDENCE_STEP_TINY};
474    ///
475    /// let proposal = ProposedFact::new(key, id, content, prov)
476    ///     .with_confidence(0.5)                        // baseline
477    ///     .adjust_confidence(CONFIDENCE_STEP_MAJOR)    // primary criterion met
478    ///     .adjust_confidence(CONFIDENCE_STEP_MINOR)    // supporting criterion met
479    ///     .adjust_confidence(CONFIDENCE_STEP_TINY);    // tiebreaker bonus
480    /// ```
481    ///
482    /// Prefer this over accumulating a local `f64` and calling `with_confidence`
483    /// at the end — the clamping is automatic and the intent is explicit at each step.
484    #[must_use]
485    pub fn adjust_confidence(mut self, delta: f64) -> Self {
486        self.confidence = (self.confidence + delta).clamp(0.0, 1.0);
487        self
488    }
489}
490
491/// Tiny confidence step — use for marginal or tiebreaker criteria (0.05).
492pub const CONFIDENCE_STEP_TINY: f64 = 0.05;
493
494/// Minor confidence step — use for supporting criteria (0.1).
495pub const CONFIDENCE_STEP_MINOR: f64 = 0.1;
496
497/// Medium confidence step — use for moderately significant criteria (0.15).
498pub const CONFIDENCE_STEP_MEDIUM: f64 = 0.15;
499
500/// Major confidence step — use for significant criteria (0.2).
501pub const CONFIDENCE_STEP_MAJOR: f64 = 0.2;
502
503/// Primary confidence step — use for decisive or high-weight criteria (0.25).
504pub const CONFIDENCE_STEP_PRIMARY: f64 = 0.25;
505
506/// Error when a `ProposedFact` fails validation.
507#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
508pub struct ValidationError {
509    /// Reason the proposal was rejected.
510    pub reason: String,
511}
512
513impl std::fmt::Display for ValidationError {
514    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
515        write!(f, "validation failed: {}", self.reason)
516    }
517}
518
519impl std::error::Error for ValidationError {}
520
521#[cfg(test)]
522mod tests {
523    use super::*;
524
525    #[test]
526    fn trace_link_local_is_replay_eligible() {
527        let local = FactTraceLink::Local(FactLocalTrace {
528            trace_id: "t1".into(),
529            span_id: "s1".into(),
530            parent_span_id: None,
531            sampled: true,
532        });
533        assert!(local.is_replay_eligible());
534    }
535
536    #[test]
537    fn trace_link_remote_is_not_replay_eligible() {
538        let remote = FactTraceLink::Remote(FactRemoteTrace {
539            system: "datadog".into(),
540            reference: "ref-1".into(),
541            retrieval_auth: None,
542            retention_hint: None,
543        });
544        assert!(!remote.is_replay_eligible());
545    }
546
547    #[cfg(feature = "kernel-authority")]
548    #[test]
549    fn promotion_record_delegates_replay_eligibility() {
550        let local_record = FactPromotionRecord::new(
551            "gate-1",
552            ContentHash::from_hex(
553                "1111111111111111111111111111111111111111111111111111111111111111",
554            ),
555            FactActor::new("actor-1", FactActorKind::Human),
556            FactValidationSummary::default(),
557            Vec::new(),
558            FactTraceLink::Local(FactLocalTrace::new("t1", "s1", None, true)),
559            "2026-01-01T00:00:00Z",
560        );
561        assert!(local_record.is_replay_eligible());
562
563        let remote_record = FactPromotionRecord::new(
564            "gate-2",
565            ContentHash::from_hex(
566                "2222222222222222222222222222222222222222222222222222222222222222",
567            ),
568            FactActor::new("actor-2", FactActorKind::System),
569            FactValidationSummary::default(),
570            Vec::new(),
571            FactTraceLink::Remote(FactRemoteTrace::new("dd", "ref-1", None, None)),
572            "2026-01-01T00:00:00Z",
573        );
574        assert!(!remote_record.is_replay_eligible());
575    }
576
577    #[cfg(feature = "kernel-authority")]
578    #[test]
579    fn fact_delegates_replay_eligibility() {
580        let fact = kernel_authority::new_fact(ContextKey::Seeds, "f1", "content");
581        assert!(fact.is_replay_eligible());
582    }
583
584    #[test]
585    fn proposed_fact_new_sets_fields() {
586        let pf = ProposedFact::new(ContextKey::Hypotheses, "p1", "my content", "gpt-4");
587        assert_eq!(pf.key, ContextKey::Hypotheses);
588        assert_eq!(pf.id, "p1");
589        assert_eq!(pf.content, "my content");
590        assert_eq!(pf.confidence(), 1.0);
591        assert_eq!(pf.provenance, "gpt-4");
592    }
593
594    #[test]
595    fn proposed_fact_with_confidence() {
596        let pf = ProposedFact::new(ContextKey::Signals, "p2", "c", "prov").with_confidence(0.42);
597        assert!((pf.confidence() - 0.42).abs() < f64::EPSILON);
598    }
599
600    #[test]
601    fn adjust_confidence_accumulates() {
602        let pf = ProposedFact::new(ContextKey::Seeds, "p", "c", "x")
603            .with_confidence(0.5)
604            .adjust_confidence(CONFIDENCE_STEP_MINOR)
605            .adjust_confidence(CONFIDENCE_STEP_MAJOR);
606        assert!((pf.confidence() - 0.8).abs() < f64::EPSILON);
607    }
608
609    #[test]
610    fn adjust_confidence_clamps_at_one() {
611        let pf = ProposedFact::new(ContextKey::Seeds, "p", "c", "x")
612            .with_confidence(0.9)
613            .adjust_confidence(CONFIDENCE_STEP_MAJOR);
614        assert_eq!(pf.confidence(), 1.0);
615    }
616
617    #[test]
618    fn adjust_confidence_clamps_at_zero() {
619        let pf = ProposedFact::new(ContextKey::Seeds, "p", "c", "x")
620            .with_confidence(0.1)
621            .adjust_confidence(-0.5);
622        assert_eq!(pf.confidence(), 0.0);
623    }
624
625    #[test]
626    fn with_confidence_clamps_high() {
627        let pf = ProposedFact::new(ContextKey::Seeds, "p", "c", "x").with_confidence(1.5);
628        assert_eq!(pf.confidence(), 1.0);
629    }
630
631    #[test]
632    fn with_confidence_clamps_negative() {
633        let pf = ProposedFact::new(ContextKey::Seeds, "p", "c", "x").with_confidence(-0.1);
634        assert_eq!(pf.confidence(), 0.0);
635    }
636
637    #[test]
638    fn with_confidence_normalizes_nan() {
639        let pf = ProposedFact::new(ContextKey::Seeds, "p", "c", "x").with_confidence(f64::NAN);
640        assert_eq!(pf.confidence(), 0.0);
641    }
642
643    #[test]
644    fn with_confidence_normalizes_infinity() {
645        let pf = ProposedFact::new(ContextKey::Seeds, "p", "c", "x").with_confidence(f64::INFINITY);
646        assert_eq!(pf.confidence(), 0.0);
647    }
648
649    #[test]
650    fn validation_error_display() {
651        let err = ValidationError {
652            reason: "bad input".into(),
653        };
654        assert_eq!(err.to_string(), "validation failed: bad input");
655    }
656
657    #[test]
658    fn validation_error_is_std_error() {
659        let err = ValidationError {
660            reason: "test".into(),
661        };
662        let _: &dyn std::error::Error = &err;
663    }
664
665    #[cfg(feature = "kernel-authority")]
666    #[test]
667    fn fact_accessors() {
668        let fact = kernel_authority::new_fact(ContextKey::Constraints, "f2", "body");
669        assert_eq!(fact.key(), ContextKey::Constraints);
670        assert_eq!(fact.id, "f2");
671        assert_eq!(fact.content, "body");
672        assert_eq!(fact.created_at(), "1970-01-01T00:00:00Z");
673        assert_eq!(fact.promotion_record().gate_id(), "kernel-authority");
674    }
675
676    #[cfg(feature = "kernel-authority")]
677    #[test]
678    fn fact_actor_accessors() {
679        let actor = FactActor::new("agent-x", FactActorKind::Suggestor);
680        assert_eq!(actor.id(), "agent-x");
681        assert_eq!(actor.kind(), FactActorKind::Suggestor);
682    }
683
684    #[cfg(feature = "kernel-authority")]
685    #[test]
686    fn validation_summary_accessors() {
687        let vs = FactValidationSummary::new(
688            vec!["check-a".into()],
689            vec!["check-b".into()],
690            vec!["warn-c".into()],
691        );
692        assert_eq!(vs.checks_passed(), &["check-a"]);
693        assert_eq!(vs.checks_skipped(), &["check-b"]);
694        assert_eq!(vs.warnings(), &["warn-c"]);
695    }
696
697    #[cfg(feature = "kernel-authority")]
698    #[test]
699    fn local_trace_accessors() {
700        let lt = FactLocalTrace::new("trace-1", "span-1", Some("parent-1".into()), false);
701        assert_eq!(lt.trace_id(), "trace-1");
702        assert_eq!(lt.span_id(), "span-1");
703        assert_eq!(lt.parent_span_id().map(SpanId::as_str), Some("parent-1"));
704        assert!(!lt.sampled());
705    }
706
707    #[cfg(feature = "kernel-authority")]
708    #[test]
709    fn remote_trace_accessors() {
710        let rt = FactRemoteTrace::new("sys", "ref", Some("auth".into()), Some("30d".into()));
711        assert_eq!(rt.system(), "sys");
712        assert_eq!(rt.reference(), "ref");
713        assert_eq!(rt.retrieval_auth(), Some("auth"));
714        assert_eq!(rt.retention_hint(), Some("30d"));
715    }
716
717    mod prop {
718        use super::*;
719        use proptest::prelude::*;
720
721        fn arb_context_key() -> impl Strategy<Value = ContextKey> {
722            prop_oneof![
723                Just(ContextKey::Seeds),
724                Just(ContextKey::Hypotheses),
725                Just(ContextKey::Strategies),
726                Just(ContextKey::Constraints),
727                Just(ContextKey::Signals),
728                Just(ContextKey::Competitors),
729                Just(ContextKey::Evaluations),
730                Just(ContextKey::Proposals),
731                Just(ContextKey::Diagnostic),
732            ]
733        }
734
735        proptest! {
736            #[test]
737            fn proposed_fact_always_constructible(
738                key in arb_context_key(),
739                id in "[a-z]{1,20}",
740                content in ".*",
741                prov in "[a-z0-9-]{1,30}",
742            ) {
743                let pf = ProposedFact::new(key, id.clone(), content.clone(), prov.clone());
744                prop_assert_eq!(pf.key, key);
745                prop_assert_eq!(&pf.id, &id);
746                prop_assert_eq!(&pf.content, &content);
747                prop_assert_eq!(&pf.provenance, &prov);
748                prop_assert!((pf.confidence() - 1.0).abs() < f64::EPSILON);
749            }
750        }
751    }
752}