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;
13
14/// Actor kind recorded on a promoted fact.
15#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize)]
16pub enum FactActorKind {
17    /// Human approver.
18    Human,
19    /// Suggestor or automated domain actor.
20    Suggestor,
21    /// Kernel or system component.
22    System,
23}
24
25/// Read-only actor record attached to authoritative facts.
26#[derive(Debug, Clone, PartialEq, Eq, Serialize)]
27pub struct FactActor {
28    id: String,
29    kind: FactActorKind,
30}
31
32impl FactActor {
33    /// Returns the actor identifier.
34    #[must_use]
35    pub fn id(&self) -> &str {
36        &self.id
37    }
38
39    /// Returns the actor kind.
40    #[must_use]
41    pub fn kind(&self) -> FactActorKind {
42        self.kind
43    }
44
45    #[cfg(feature = "kernel-authority")]
46    #[doc(hidden)]
47    pub fn new(id: impl Into<String>, kind: FactActorKind) -> Self {
48        Self {
49            id: id.into(),
50            kind,
51        }
52    }
53}
54
55/// Summary of validation checks attached to an authoritative fact.
56#[derive(Debug, Clone, PartialEq, Eq, Serialize, Default)]
57pub struct FactValidationSummary {
58    checks_passed: Vec<String>,
59    checks_skipped: Vec<String>,
60    warnings: Vec<String>,
61}
62
63impl FactValidationSummary {
64    /// Returns validation checks that passed.
65    #[must_use]
66    pub fn checks_passed(&self) -> &[String] {
67        &self.checks_passed
68    }
69
70    /// Returns validation checks that were skipped.
71    #[must_use]
72    pub fn checks_skipped(&self) -> &[String] {
73        &self.checks_skipped
74    }
75
76    /// Returns validation warnings.
77    #[must_use]
78    pub fn warnings(&self) -> &[String] {
79        &self.warnings
80    }
81
82    #[cfg(feature = "kernel-authority")]
83    #[doc(hidden)]
84    pub fn new(
85        checks_passed: Vec<String>,
86        checks_skipped: Vec<String>,
87        warnings: Vec<String>,
88    ) -> Self {
89        Self {
90            checks_passed,
91            checks_skipped,
92            warnings,
93        }
94    }
95}
96
97/// Typed evidence references attached to an authoritative fact.
98#[derive(Debug, Clone, PartialEq, Eq, Serialize)]
99#[serde(tag = "type", content = "id")]
100pub enum FactEvidenceRef {
101    /// Observation used as evidence.
102    Observation(String),
103    /// Human approval used as evidence.
104    HumanApproval(String),
105    /// Derived artifact used as evidence.
106    Derived(String),
107}
108
109/// Local replayable trace reference.
110#[derive(Debug, Clone, PartialEq, Eq, Serialize)]
111pub struct FactLocalTrace {
112    trace_id: String,
113    span_id: String,
114    parent_span_id: Option<String>,
115    sampled: bool,
116}
117
118impl FactLocalTrace {
119    /// Returns the trace identifier.
120    #[must_use]
121    pub fn trace_id(&self) -> &str {
122        &self.trace_id
123    }
124
125    /// Returns the span identifier.
126    #[must_use]
127    pub fn span_id(&self) -> &str {
128        &self.span_id
129    }
130
131    /// Returns the parent span identifier.
132    #[must_use]
133    pub fn parent_span_id(&self) -> Option<&str> {
134        self.parent_span_id.as_deref()
135    }
136
137    /// Returns whether the trace was sampled.
138    #[must_use]
139    pub fn sampled(&self) -> bool {
140        self.sampled
141    }
142
143    #[cfg(feature = "kernel-authority")]
144    #[doc(hidden)]
145    pub fn new(
146        trace_id: impl Into<String>,
147        span_id: impl Into<String>,
148        parent_span_id: Option<String>,
149        sampled: bool,
150    ) -> Self {
151        Self {
152            trace_id: trace_id.into(),
153            span_id: span_id.into(),
154            parent_span_id,
155            sampled,
156        }
157    }
158}
159
160/// Remote audit-only trace reference.
161#[derive(Debug, Clone, PartialEq, Eq, Serialize)]
162pub struct FactRemoteTrace {
163    system: String,
164    reference: String,
165    retrieval_auth: Option<String>,
166    retention_hint: Option<String>,
167}
168
169impl FactRemoteTrace {
170    /// Returns the remote system identifier.
171    #[must_use]
172    pub fn system(&self) -> &str {
173        &self.system
174    }
175
176    /// Returns the remote trace reference.
177    #[must_use]
178    pub fn reference(&self) -> &str {
179        &self.reference
180    }
181
182    /// Returns the retrieval auth hint.
183    #[must_use]
184    pub fn retrieval_auth(&self) -> Option<&str> {
185        self.retrieval_auth.as_deref()
186    }
187
188    /// Returns the retention hint.
189    #[must_use]
190    pub fn retention_hint(&self) -> Option<&str> {
191        self.retention_hint.as_deref()
192    }
193
194    #[cfg(feature = "kernel-authority")]
195    #[doc(hidden)]
196    pub fn new(
197        system: impl Into<String>,
198        reference: impl Into<String>,
199        retrieval_auth: Option<String>,
200        retention_hint: Option<String>,
201    ) -> Self {
202        Self {
203            system: system.into(),
204            reference: reference.into(),
205            retrieval_auth,
206            retention_hint,
207        }
208    }
209}
210
211/// Trace record attached to an authoritative fact.
212#[derive(Debug, Clone, PartialEq, Eq, Serialize)]
213#[serde(tag = "type")]
214pub enum FactTraceLink {
215    /// Local replayable trace.
216    Local(FactLocalTrace),
217    /// Remote audit-only trace.
218    Remote(FactRemoteTrace),
219}
220
221impl FactTraceLink {
222    /// Returns whether the trace is replay-eligible.
223    #[must_use]
224    pub fn is_replay_eligible(&self) -> bool {
225        matches!(self, Self::Local(_))
226    }
227}
228
229/// Read-only promotion record attached to an authoritative fact.
230#[derive(Debug, Clone, PartialEq, Eq, Serialize)]
231pub struct FactPromotionRecord {
232    gate_id: String,
233    policy_version_hash: String,
234    approver: FactActor,
235    validation_summary: FactValidationSummary,
236    evidence_refs: Vec<FactEvidenceRef>,
237    trace_link: FactTraceLink,
238    promoted_at: String,
239}
240
241impl FactPromotionRecord {
242    /// Returns the gate identifier that promoted the fact.
243    #[must_use]
244    pub fn gate_id(&self) -> &str {
245        &self.gate_id
246    }
247
248    /// Returns the policy hash used during promotion.
249    #[must_use]
250    pub fn policy_version_hash(&self) -> &str {
251        &self.policy_version_hash
252    }
253
254    /// Returns the approving actor.
255    #[must_use]
256    pub fn approver(&self) -> &FactActor {
257        &self.approver
258    }
259
260    /// Returns the validation summary.
261    #[must_use]
262    pub fn validation_summary(&self) -> &FactValidationSummary {
263        &self.validation_summary
264    }
265
266    /// Returns the evidence references used during promotion.
267    #[must_use]
268    pub fn evidence_refs(&self) -> &[FactEvidenceRef] {
269        &self.evidence_refs
270    }
271
272    /// Returns the trace link for audit or replay.
273    #[must_use]
274    pub fn trace_link(&self) -> &FactTraceLink {
275        &self.trace_link
276    }
277
278    /// Returns the promotion timestamp.
279    #[must_use]
280    pub fn promoted_at(&self) -> &str {
281        &self.promoted_at
282    }
283
284    /// Returns whether the promotion is replay-eligible.
285    #[must_use]
286    pub fn is_replay_eligible(&self) -> bool {
287        self.trace_link.is_replay_eligible()
288    }
289
290    #[cfg(feature = "kernel-authority")]
291    #[doc(hidden)]
292    pub fn new(
293        gate_id: impl Into<String>,
294        policy_version_hash: impl Into<String>,
295        approver: FactActor,
296        validation_summary: FactValidationSummary,
297        evidence_refs: Vec<FactEvidenceRef>,
298        trace_link: FactTraceLink,
299        promoted_at: impl Into<String>,
300    ) -> Self {
301        Self {
302            gate_id: gate_id.into(),
303            policy_version_hash: policy_version_hash.into(),
304            approver,
305            validation_summary,
306            evidence_refs,
307            trace_link,
308            promoted_at: promoted_at.into(),
309        }
310    }
311}
312
313/// A validated, authoritative assertion in the context.
314///
315/// Facts are append-only. Once added to the context, they are never
316/// mutated or removed (within a convergence run). History is preserved.
317#[derive(Debug, Clone, PartialEq, Eq, Serialize)]
318pub struct Fact {
319    /// Which context key this fact belongs to.
320    key: ContextKey,
321    /// Unique identifier within the context key namespace.
322    pub id: String,
323    /// The fact's content as a string. Interpretation is key-dependent.
324    pub content: String,
325    /// The immutable promotion record that made this fact authoritative.
326    promotion_record: FactPromotionRecord,
327    /// When the authoritative fact entered context.
328    created_at: String,
329}
330
331impl Fact {
332    /// Returns the context key this fact belongs to.
333    #[must_use]
334    pub fn key(&self) -> ContextKey {
335        self.key
336    }
337
338    /// Returns the immutable promotion record for this fact.
339    #[must_use]
340    pub fn promotion_record(&self) -> &FactPromotionRecord {
341        &self.promotion_record
342    }
343
344    /// Returns the fact creation timestamp.
345    #[must_use]
346    pub fn created_at(&self) -> &str {
347        &self.created_at
348    }
349
350    /// Returns whether the fact is replay-eligible.
351    #[must_use]
352    pub fn is_replay_eligible(&self) -> bool {
353        self.promotion_record.is_replay_eligible()
354    }
355}
356
357/// Kernel-only construction helpers for authoritative facts.
358#[cfg(feature = "kernel-authority")]
359#[doc(hidden)]
360pub mod kernel_authority {
361    use super::*;
362
363    /// Creates a kernel-authoritative fact with default promotion metadata.
364    #[must_use]
365    pub fn new_fact(key: ContextKey, id: impl Into<String>, content: impl Into<String>) -> Fact {
366        new_fact_with_promotion(
367            key,
368            id,
369            content,
370            FactPromotionRecord::new(
371                "kernel-authority",
372                "0000000000000000000000000000000000000000000000000000000000000000",
373                FactActor::new("converge-kernel", FactActorKind::System),
374                FactValidationSummary::default(),
375                Vec::new(),
376                FactTraceLink::Local(FactLocalTrace::new("kernel-authority", "seed", None, true)),
377                "1970-01-01T00:00:00Z",
378            ),
379            "1970-01-01T00:00:00Z",
380        )
381    }
382
383    /// Creates a kernel-authoritative fact with an explicit promotion record.
384    #[must_use]
385    pub fn new_fact_with_promotion(
386        key: ContextKey,
387        id: impl Into<String>,
388        content: impl Into<String>,
389        promotion_record: FactPromotionRecord,
390        created_at: impl Into<String>,
391    ) -> Fact {
392        Fact {
393            key,
394            id: id.into(),
395            content: content.into(),
396            promotion_record,
397            created_at: created_at.into(),
398        }
399    }
400}
401
402/// An unvalidated suggestion from a non-authoritative source.
403///
404/// Proposed facts live in `ContextKey::Proposals` until a `ValidationAgent`
405/// promotes them to `Fact`. The proposal tracks its origin for audit trail.
406#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
407pub struct ProposedFact {
408    /// The context key this proposal targets.
409    pub key: ContextKey,
410    /// Unique identifier encoding origin and target.
411    pub id: String,
412    /// The proposed content.
413    pub content: String,
414    /// Confidence hint from the source (0.0 - 1.0).
415    pub confidence: f64,
416    /// Provenance information (e.g., model ID, prompt hash).
417    pub provenance: String,
418}
419
420impl ProposedFact {
421    /// Create a new draft proposal with explicit provenance.
422    #[must_use]
423    pub fn new(
424        key: ContextKey,
425        id: impl Into<String>,
426        content: impl Into<String>,
427        provenance: impl Into<String>,
428    ) -> Self {
429        Self {
430            key,
431            id: id.into(),
432            content: content.into(),
433            confidence: 1.0,
434            provenance: provenance.into(),
435        }
436    }
437
438    /// Override the proposal confidence.
439    #[must_use]
440    pub fn with_confidence(mut self, confidence: f64) -> Self {
441        self.confidence = confidence;
442        self
443    }
444}
445
446/// Error when a `ProposedFact` fails validation.
447#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
448pub struct ValidationError {
449    /// Reason the proposal was rejected.
450    pub reason: String,
451}
452
453impl std::fmt::Display for ValidationError {
454    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
455        write!(f, "validation failed: {}", self.reason)
456    }
457}
458
459impl std::error::Error for ValidationError {}