1use chrono::{DateTime, Utc};
19use serde::{Deserialize, Serialize};
20
21use crate::ids;
22
23#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Serialize, Deserialize)]
25#[serde(rename_all = "snake_case")]
26pub enum SupportState {
27 Supported,
29 PartiallySupported,
31 Unsupported,
33 Contradicted,
35 HeuristicOnly,
37 Unknown,
39}
40
41impl SupportState {
42 pub fn is_strong(&self) -> bool {
45 matches!(self, Self::Supported | Self::PartiallySupported)
46 }
47}
48
49#[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#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
63#[serde(rename_all = "snake_case")]
64pub enum SupportAdmissionMethod {
65 OperatorAdmitted,
67 TestFixtureAdmitted,
69 ExternalReceiptAdmitted,
71}
72
73#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Serialize, Deserialize)]
75#[serde(rename_all = "snake_case")]
76pub enum ContradictionResolution {
77 Confirmed,
79 Rejected,
81 Superseded,
83}
84
85#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
87#[serde(rename_all = "snake_case")]
88pub enum ContradictionStatus {
89 Candidate,
91 UnderReview,
93 Unresolved,
95 Confirmed,
97 Rejected,
99 Superseded,
101}
102
103#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
105#[serde(rename_all = "snake_case")]
106pub enum ProofDebt {
107 MissingSourceBasis,
109 MissingBenchmark,
111 MissingRepro,
113 MissingExternalValidation,
115 None,
117}
118
119#[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#[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#[derive(Debug, Clone, Serialize, Deserialize)]
145pub struct SourceIndex {
146 pub sources: Vec<SourceArtifact>,
147 pub spans: Vec<SourceSpan>,
148}
149
150#[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#[derive(Debug, Clone, Serialize, Deserialize)]
163pub struct EvidenceBundle {
164 pub evidence_bundle_id: String,
166 pub claim_id: String,
168 pub evidence_links: Vec<EvidenceLink>,
170 pub omitted_evidence_refs: Vec<String>,
172 pub created_recorded_time: DateTime<Utc>,
174}
175
176impl EvidenceBundle {
177 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#[derive(Debug, Clone, Serialize, Deserialize)]
191pub struct SupportJudgment {
192 pub support_judgment_id: String,
194 pub claim_id: String,
196 pub evidence_bundle_ref: String,
198 pub support_state: SupportState,
200 pub method: String,
202 pub rationale: String,
204 pub contradiction_refs: Vec<String>,
206 pub proof_debt: Vec<ProofDebt>,
208 pub created_recorded_time: DateTime<Utc>,
210}
211
212#[derive(Debug, Clone, Serialize, Deserialize, Default)]
219pub struct SupportProofPayload {
220 pub operator_ref: Option<String>,
222 pub fixture_ref: Option<String>,
224 pub expected_assertion: Option<String>,
226 pub fixture_digest: Option<String>,
228 pub external_receipt_ref: Option<String>,
230 pub external_receipt_digest: Option<String>,
232 pub source_adapter: Option<String>,
234 pub degradation_trust_note: Option<String>,
236}
237
238impl SupportProofPayload {
239 pub fn has_operator_proof(&self) -> bool {
241 self.operator_ref.is_some()
242 }
243
244 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 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#[derive(Debug, Clone, Serialize, Deserialize)]
262pub struct SupportAdmission {
263 pub claim_id: String,
265 pub previous_support_judgment_ref: String,
267 pub new_support_judgment_ref: String,
269 pub method: SupportAdmissionMethod,
271 pub admitted_support_state: SupportState,
273 pub rationale: String,
275 pub operator_ref: Option<String>,
277 pub proof_payload: Option<SupportProofPayload>,
279 pub proof_debt_waiver: Option<String>,
281 pub recorded_time: DateTime<Utc>,
283}
284
285#[derive(Debug, Clone, Serialize, Deserialize)]
287pub struct ContradictionRecord {
288 pub contradiction_id: String,
290 pub claim_refs: Vec<String>,
292 pub support_judgment_refs: Vec<String>,
294 pub source_span_refs: Vec<String>,
296 pub pattern: String,
298 pub rationale: String,
300 pub status: ContradictionStatus,
302 pub created_recorded_time: DateTime<Utc>,
304}
305
306impl ContradictionRecord {
307 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#[derive(Debug, Clone, Serialize, Deserialize)]
329pub struct ContradictionResolutionRecord {
330 pub contradiction_id: String,
332 pub previous_status: ContradictionStatus,
334 pub resolution: ContradictionResolution,
336 pub rationale: String,
338 pub affected_support_judgment_refs: Vec<String>,
340 pub supersession_receipt_refs: Vec<String>,
342 pub review_queue_refs: Vec<String>,
344 pub recorded_time: DateTime<Utc>,
346}
347
348#[derive(Debug, Clone, Serialize, Deserialize)]
350pub struct Supersession {
351 pub superseded_ref: String,
353 pub superseding_ref: String,
355 pub rationale: String,
357 pub recorded_time: DateTime<Utc>,
359}
360
361#[derive(Debug, Clone, Serialize, Deserialize)]
363pub struct Claim {
364 pub claim_id: String,
366 pub source_id: String,
368 pub span_id: String,
370 pub claim_text: String,
372 pub normalized_claim: String,
374 pub claim_type: String,
376 pub status: String,
378 pub support_judgment_ref: String,
380 pub evidence_bundle_ref: String,
382 pub proof_debt: Vec<ProofDebt>,
384 pub recommended_action: String,
386 pub rationale: String,
388 pub confidence: f64,
390 pub created_at: Option<DateTime<Utc>>,
392 pub metadata: serde_json::Value,
394}
395
396impl Claim {
397 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 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}