Skip to main content

claim_ledger/
types.rs

1//! Core domain types for the claim ledger.
2//!
3//! ## Support State Machine
4//!
5//! Claims move through support states as follows:
6//!
7//! ```text
8//! [UNKNOWN/HEURISTIC_ONLY] ──admit_support──► [SUPPORTED]
9//! [UNKNOWN/HEURISTIC_ONLY] ──admit_support──► [PARTIALLY_SUPPORTED]
10//! [UNKNOWN/HEURISTIC_ONLY] ──admit_support──► [UNSUPPORTED]
11//! [SUPPORTED/PARTIALLY_SUPPORTED] ──contradiction confirmed──► [CONTRADICTED]
12//! [CONTRADICTED] ──contradiction rejected──► [HEURISTIC_ONLY or prior state]
13//! ```
14//!
15//! Unsupported is bundle-scoped only — it signals no supporting evidence was found
16//! for this specific bundle, not a universal verdict.
17
18use chrono::{DateTime, Utc};
19use serde::{Deserialize, Serialize};
20
21use crate::ids;
22
23/// Support states for a claim within a specific evidence bundle scope.
24#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Serialize, Deserialize)]
25#[serde(rename_all = "snake_case")]
26pub enum SupportState {
27    /// Positive evidence was found in the bundle.
28    Supported,
29    /// Mixed or partial evidence was found.
30    PartiallySupported,
31    /// No supporting evidence found in this bundle.
32    Unsupported,
33    /// A resolved contradiction applies to this claim.
34    Contradicted,
35    /// Heuristic matching was used; no strong proof exists.
36    HeuristicOnly,
37    /// No support judgment has been made yet.
38    Unknown,
39}
40
41impl SupportState {
42    /// Returns true for states that constitute a "strong" admission
43    /// (i.e., have proof debt waived or a valid proof payload).
44    pub fn is_strong(&self) -> bool {
45        matches!(self, Self::Supported | Self::PartiallySupported)
46    }
47}
48
49/// Evidence relation types between evidence and claims.
50#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
51#[serde(rename_all = "snake_case")]
52pub enum EvidenceRelation {
53    Mentions,
54    Supports,
55    PartiallySupports,
56    Contradicts,
57    SourceSpanAnchorOnly,
58    RetrievedWith,
59}
60
61/// Method by which a support admission was made.
62#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
63#[serde(rename_all = "snake_case")]
64pub enum SupportAdmissionMethod {
65    /// Operator explicitly admitted the support state.
66    OperatorAdmitted,
67    /// Admitted via a test fixture.
68    TestFixtureAdmitted,
69    /// Admitted via an external receipt.
70    ExternalReceiptAdmitted,
71}
72
73/// Contradiction resolution outcomes.
74#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Serialize, Deserialize)]
75#[serde(rename_all = "snake_case")]
76pub enum ContradictionResolution {
77    /// Contradiction confirmed — affected claims move to Contradicted state.
78    Confirmed,
79    /// Contradiction rejected — affected claims retain or revert to prior state.
80    Rejected,
81    /// Contradiction superseded by a newer resolved contradiction.
82    Superseded,
83}
84
85/// Contradiction status lifecycle.
86#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
87#[serde(rename_all = "snake_case")]
88pub enum ContradictionStatus {
89    /// Contradiction candidate detected, not yet reviewed.
90    Candidate,
91    /// Under active review.
92    UnderReview,
93    /// Review complete but no resolution reached yet.
94    Unresolved,
95    /// Resolution confirmed the contradiction.
96    Confirmed,
97    /// Resolution rejected the contradiction.
98    Rejected,
99    /// Superseded by another contradiction resolution.
100    Superseded,
101}
102
103/// Types of proof debt — things that could weaken a support claim.
104#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
105#[serde(rename_all = "snake_case")]
106pub enum ProofDebt {
107    /// Claim lacks a source basis in the input.
108    MissingSourceBasis,
109    /// Claim lacks a benchmark reference.
110    MissingBenchmark,
111    /// Claim lacks a reproduction case.
112    MissingRepro,
113    /// Claim lacks external validation.
114    MissingExternalValidation,
115    /// No proof debt.
116    None,
117}
118
119/// A source artifact (document) that claims are extracted from.
120#[derive(Debug, Clone, Serialize, Deserialize)]
121pub struct SourceArtifact {
122    pub source_id: String,
123    pub path: String,
124    pub digest: String,
125    pub kind: String,
126    pub bytes_size: u64,
127    pub loaded_at: Option<DateTime<Utc>>,
128    pub degradation: Vec<String>,
129}
130
131/// A span within a source artifact (e.g., a paragraph or section).
132#[derive(Debug, Clone, Serialize, Deserialize)]
133pub struct SourceSpan {
134    pub source_id: String,
135    pub span_id: String,
136    pub start_line: Option<u32>,
137    pub end_line: Option<u32>,
138    pub heading_path: Vec<String>,
139    pub text: String,
140    pub digest: String,
141}
142
143/// A source index mapping sources and spans.
144#[derive(Debug, Clone, Serialize, Deserialize)]
145pub struct SourceIndex {
146    pub sources: Vec<SourceArtifact>,
147    pub spans: Vec<SourceSpan>,
148}
149
150/// An evidence link relating a claim to a source span.
151#[derive(Debug, Clone, Serialize, Deserialize)]
152pub struct EvidenceLink {
153    pub relation: EvidenceRelation,
154    pub source_id: String,
155    pub span_id: String,
156    pub quote: String,
157    pub digest: String,
158    pub support_role: String,
159}
160
161/// An evidence bundle attached to a claim.
162#[derive(Debug, Clone, Serialize, Deserialize)]
163pub struct EvidenceBundle {
164    /// Unique identifier for this evidence bundle.
165    pub evidence_bundle_id: String,
166    /// The claim this bundle supports.
167    pub claim_id: String,
168    /// Evidence links within this bundle.
169    pub evidence_links: Vec<EvidenceLink>,
170    /// IDs of evidence that was considered but omitted.
171    pub omitted_evidence_refs: Vec<String>,
172    /// When this bundle was created.
173    pub created_recorded_time: DateTime<Utc>,
174}
175
176impl EvidenceBundle {
177    /// Create a new evidence bundle for a claim.
178    pub fn new(claim_id: &str) -> Self {
179        Self {
180            evidence_bundle_id: ids::evidence_bundle_id(claim_id),
181            claim_id: claim_id.to_string(),
182            evidence_links: Vec::new(),
183            omitted_evidence_refs: Vec::new(),
184            created_recorded_time: Utc::now(),
185        }
186    }
187}
188
189/// A support judgment scoped to a claim via an evidence bundle.
190#[derive(Debug, Clone, Serialize, Deserialize)]
191pub struct SupportJudgment {
192    /// Unique identifier for this support judgment.
193    pub support_judgment_id: String,
194    /// The claim being judged.
195    pub claim_id: String,
196    /// The evidence bundle this judgment is scoped to.
197    pub evidence_bundle_ref: String,
198    /// The support state assigned.
199    pub support_state: SupportState,
200    /// How this judgment was determined.
201    pub method: String,
202    /// Human-readable rationale for this judgment.
203    pub rationale: String,
204    /// Contradiction IDs affecting this claim.
205    pub contradiction_refs: Vec<String>,
206    /// Proof debts attached to this judgment.
207    pub proof_debt: Vec<ProofDebt>,
208    /// When this judgment was created.
209    pub created_recorded_time: DateTime<Utc>,
210}
211
212/// Proof payload attached to a support admission.
213///
214/// Must contain method-specific fields to be considered valid:
215/// - Operator: `operator_ref`
216/// - Test fixture: `fixture_ref`, `expected_assertion`, `fixture_digest`
217/// - External receipt: `external_receipt_ref`, `external_receipt_digest`, `source_adapter`, `degradation_trust_note`
218#[derive(Debug, Clone, Serialize, Deserialize, Default)]
219pub struct SupportProofPayload {
220    /// Operator reference for operator-admitted support.
221    pub operator_ref: Option<String>,
222    /// Test fixture reference for fixture-admitted support.
223    pub fixture_ref: Option<String>,
224    /// Expected assertion for fixture-based support.
225    pub expected_assertion: Option<String>,
226    /// Digest of the fixture used.
227    pub fixture_digest: Option<String>,
228    /// External receipt reference.
229    pub external_receipt_ref: Option<String>,
230    /// Digest of the external receipt.
231    pub external_receipt_digest: Option<String>,
232    /// Source adapter name for external receipts.
233    pub source_adapter: Option<String>,
234    /// Trust note for degraded external receipts.
235    pub degradation_trust_note: Option<String>,
236}
237
238impl SupportProofPayload {
239    /// Returns true if this payload has operator proof.
240    pub fn has_operator_proof(&self) -> bool {
241        self.operator_ref.is_some()
242    }
243
244    /// Returns true if this payload has fixture proof.
245    pub fn has_fixture_proof(&self) -> bool {
246        self.fixture_ref.is_some()
247            && self.expected_assertion.is_some()
248            && self.fixture_digest.is_some()
249    }
250
251    /// Returns true if this payload has external receipt proof.
252    pub fn has_external_receipt_proof(&self) -> bool {
253        self.external_receipt_ref.is_some()
254            && self.external_receipt_digest.is_some()
255            && self.source_adapter.is_some()
256            && self.degradation_trust_note.is_some()
257    }
258}
259
260/// An admission record for a support state upgrade.
261#[derive(Debug, Clone, Serialize, Deserialize)]
262pub struct SupportAdmission {
263    /// The claim being admitted.
264    pub claim_id: String,
265    /// The previous support judgment (superseded).
266    pub previous_support_judgment_ref: String,
267    /// The new support judgment (superseding).
268    pub new_support_judgment_ref: String,
269    /// The admission method used.
270    pub method: SupportAdmissionMethod,
271    /// The support state admitted.
272    pub admitted_support_state: SupportState,
273    /// Rationale for the admission.
274    pub rationale: String,
275    /// Operator reference (if operator-admitted).
276    pub operator_ref: Option<String>,
277    /// Optional proof payload.
278    pub proof_payload: Option<SupportProofPayload>,
279    /// Optional explicit proof debt waiver.
280    pub proof_debt_waiver: Option<String>,
281    /// When this admission was recorded.
282    pub recorded_time: DateTime<Utc>,
283}
284
285/// A contradiction between two or more claims.
286#[derive(Debug, Clone, Serialize, Deserialize)]
287pub struct ContradictionRecord {
288    /// Unique identifier for this contradiction.
289    pub contradiction_id: String,
290    /// IDs of claims involved in this contradiction.
291    pub claim_refs: Vec<String>,
292    /// Support judgment IDs involved.
293    pub support_judgment_refs: Vec<String>,
294    /// Source span IDs involved.
295    pub source_span_refs: Vec<String>,
296    /// The pattern/kind of contradiction detected.
297    pub pattern: String,
298    /// Rationale for why this is a contradiction.
299    pub rationale: String,
300    /// Current lifecycle status.
301    pub status: ContradictionStatus,
302    /// When this record was created.
303    pub created_recorded_time: DateTime<Utc>,
304}
305
306impl ContradictionRecord {
307    /// Create a new contradiction record between two claims.
308    pub fn new(left_claim_id: &str, right_claim_id: &str, pattern: &str, rationale: &str) -> Self {
309        let claim_refs = if left_claim_id <= right_claim_id {
310            vec![left_claim_id.to_string(), right_claim_id.to_string()]
311        } else {
312            vec![right_claim_id.to_string(), left_claim_id.to_string()]
313        };
314        Self {
315            contradiction_id: ids::contradiction_id(left_claim_id, right_claim_id, pattern),
316            claim_refs,
317            support_judgment_refs: Vec::new(),
318            source_span_refs: Vec::new(),
319            pattern: pattern.to_string(),
320            rationale: rationale.to_string(),
321            status: ContradictionStatus::Candidate,
322            created_recorded_time: Utc::now(),
323        }
324    }
325}
326
327/// Resolution record for a contradiction.
328#[derive(Debug, Clone, Serialize, Deserialize)]
329pub struct ContradictionResolutionRecord {
330    /// The contradiction being resolved.
331    pub contradiction_id: String,
332    /// Status before resolution.
333    pub previous_status: ContradictionStatus,
334    /// The resolution outcome.
335    pub resolution: ContradictionResolution,
336    /// Rationale for the resolution.
337    pub rationale: String,
338    /// Support judgment IDs affected by this resolution.
339    pub affected_support_judgment_refs: Vec<String>,
340    /// Supersession receipt IDs created.
341    pub supersession_receipt_refs: Vec<String>,
342    /// Review queue item IDs affected.
343    pub review_queue_refs: Vec<String>,
344    /// When this resolution was recorded.
345    pub recorded_time: DateTime<Utc>,
346}
347
348/// A supersession record linking a previous ID to a new ID.
349#[derive(Debug, Clone, Serialize, Deserialize)]
350pub struct Supersession {
351    /// The ID that was superseded.
352    pub superseded_ref: String,
353    /// The ID that superseded it.
354    pub superseding_ref: String,
355    /// Rationale for the supersession.
356    pub rationale: String,
357    /// When this supersession was recorded.
358    pub recorded_time: DateTime<Utc>,
359}
360
361/// A claim extracted from source material.
362#[derive(Debug, Clone, Serialize, Deserialize)]
363pub struct Claim {
364    /// Unique identifier for this claim.
365    pub claim_id: String,
366    /// Source this claim was extracted from.
367    pub source_id: String,
368    /// Span within the source this claim was extracted from.
369    pub span_id: String,
370    /// The raw claim text.
371    pub claim_text: String,
372    /// Normalized form of the claim text.
373    pub normalized_claim: String,
374    /// Kind of claim (e.g., "fact", "suggestion", "warning").
375    pub claim_type: String,
376    /// Processing status.
377    pub status: String,
378    /// Current support judgment ID for this claim.
379    pub support_judgment_ref: String,
380    /// Evidence bundle ID for this claim.
381    pub evidence_bundle_ref: String,
382    /// Proof debts attached to this claim.
383    pub proof_debt: Vec<ProofDebt>,
384    /// Recommended next action.
385    pub recommended_action: String,
386    /// Rationale for this claim.
387    pub rationale: String,
388    /// Confidence score 0.0–1.0.
389    pub confidence: f64,
390    /// When this claim was created.
391    pub created_at: Option<DateTime<Utc>>,
392    /// Additional metadata.
393    pub metadata: serde_json::Value,
394}
395
396impl Claim {
397    /// Create a new claim from source and span.
398    pub fn new(source_id: &str, span_id: &str, claim_text: &str, claim_type: &str) -> Self {
399        let normalized = ids::normalize_text(claim_text);
400        Self {
401            claim_id: ids::claim_id(source_id, span_id, &normalized),
402            source_id: source_id.to_string(),
403            span_id: span_id.to_string(),
404            claim_text: claim_text.to_string(),
405            normalized_claim: normalized,
406            claim_type: claim_type.to_string(),
407            status: "pending".to_string(),
408            support_judgment_ref: String::new(),
409            evidence_bundle_ref: String::new(),
410            proof_debt: vec![ProofDebt::MissingSourceBasis],
411            recommended_action: "await_support_judgment".to_string(),
412            rationale: String::new(),
413            confidence: 0.0,
414            created_at: None,
415            metadata: serde_json::Value::Object(Default::default()),
416        }
417    }
418}
419
420#[cfg(test)]
421mod tests {
422    use super::*;
423
424    #[test]
425    fn support_state_is_strong() {
426        assert!(SupportState::Supported.is_strong());
427        assert!(SupportState::PartiallySupported.is_strong());
428        assert!(!SupportState::Unsupported.is_strong());
429        assert!(!SupportState::Contradicted.is_strong());
430        assert!(!SupportState::HeuristicOnly.is_strong());
431        assert!(!SupportState::Unknown.is_strong());
432    }
433
434    #[test]
435    fn evidence_bundle_new_has_id_prefix() {
436        let bundle = EvidenceBundle::new("clm_abc123");
437        assert!(bundle.evidence_bundle_id.starts_with("evb_"));
438        assert_eq!(bundle.claim_id, "clm_abc123");
439    }
440
441    #[test]
442    fn contradiction_record_new_is_ordered() {
443        let c1 = ContradictionRecord::new("clm_a", "clm_b", "exact", "texts contradict");
444        let c2 = ContradictionRecord::new("clm_b", "clm_a", "exact", "texts contradict");
445        // Ordering ensures same result regardless of argument order
446        assert_eq!(c1.contradiction_id, c2.contradiction_id);
447        assert_eq!(c1.claim_refs, c2.claim_refs);
448    }
449
450    #[test]
451    fn support_proof_payload_has_methods() {
452        let operator = SupportProofPayload {
453            operator_ref: Some("op-123".to_string()),
454            ..Default::default()
455        };
456        assert!(operator.has_operator_proof());
457
458        let fixture = SupportProofPayload {
459            fixture_ref: Some("fix-456".to_string()),
460            expected_assertion: Some("claim is true".to_string()),
461            fixture_digest: Some("abc123".to_string()),
462            ..Default::default()
463        };
464        assert!(fixture.has_fixture_proof());
465    }
466
467    #[test]
468    fn claim_new_has_deterministic_id() {
469        let c1 = Claim::new("src1", "sp1", "The sky is blue", "fact");
470        let c2 = Claim::new("src1", "sp1", "The sky is blue", "fact");
471        assert_eq!(c1.claim_id, c2.claim_id);
472    }
473}