use chrono::{DateTime, Utc};
use serde::{Deserialize, Serialize};
use crate::ids;
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Serialize, Deserialize)]
#[serde(rename_all = "snake_case")]
pub enum SupportState {
Supported,
PartiallySupported,
Unsupported,
Contradicted,
HeuristicOnly,
Unknown,
}
impl SupportState {
pub fn is_strong(&self) -> bool {
matches!(self, Self::Supported | Self::PartiallySupported)
}
}
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
#[serde(rename_all = "snake_case")]
pub enum EvidenceRelation {
Mentions,
Supports,
PartiallySupports,
Contradicts,
SourceSpanAnchorOnly,
RetrievedWith,
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
#[serde(rename_all = "snake_case")]
pub enum SupportAdmissionMethod {
OperatorAdmitted,
TestFixtureAdmitted,
ExternalReceiptAdmitted,
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Serialize, Deserialize)]
#[serde(rename_all = "snake_case")]
pub enum ContradictionResolution {
Confirmed,
Rejected,
Superseded,
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
#[serde(rename_all = "snake_case")]
pub enum ContradictionStatus {
Candidate,
UnderReview,
Unresolved,
Confirmed,
Rejected,
Superseded,
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
#[serde(rename_all = "snake_case")]
pub enum ProofDebt {
MissingSourceBasis,
MissingBenchmark,
MissingRepro,
MissingExternalValidation,
None,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct SourceArtifact {
pub source_id: String,
pub path: String,
pub digest: String,
pub kind: String,
pub bytes_size: u64,
pub loaded_at: Option<DateTime<Utc>>,
pub degradation: Vec<String>,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct SourceSpan {
pub source_id: String,
pub span_id: String,
pub start_line: Option<u32>,
pub end_line: Option<u32>,
pub heading_path: Vec<String>,
pub text: String,
pub digest: String,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct SourceIndex {
pub sources: Vec<SourceArtifact>,
pub spans: Vec<SourceSpan>,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct EvidenceLink {
pub relation: EvidenceRelation,
pub source_id: String,
pub span_id: String,
pub quote: String,
pub digest: String,
pub support_role: String,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct EvidenceBundle {
pub evidence_bundle_id: String,
pub claim_id: String,
pub evidence_links: Vec<EvidenceLink>,
pub omitted_evidence_refs: Vec<String>,
pub created_recorded_time: DateTime<Utc>,
}
impl EvidenceBundle {
pub fn new(claim_id: &str) -> Self {
Self {
evidence_bundle_id: ids::evidence_bundle_id(claim_id),
claim_id: claim_id.to_string(),
evidence_links: Vec::new(),
omitted_evidence_refs: Vec::new(),
created_recorded_time: Utc::now(),
}
}
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct SupportJudgment {
pub support_judgment_id: String,
pub claim_id: String,
pub evidence_bundle_ref: String,
pub support_state: SupportState,
pub method: String,
pub rationale: String,
pub contradiction_refs: Vec<String>,
pub proof_debt: Vec<ProofDebt>,
pub created_recorded_time: DateTime<Utc>,
}
#[derive(Debug, Clone, Serialize, Deserialize, Default)]
pub struct SupportProofPayload {
pub operator_ref: Option<String>,
pub fixture_ref: Option<String>,
pub expected_assertion: Option<String>,
pub fixture_digest: Option<String>,
pub external_receipt_ref: Option<String>,
pub external_receipt_digest: Option<String>,
pub source_adapter: Option<String>,
pub degradation_trust_note: Option<String>,
}
impl SupportProofPayload {
pub fn has_operator_proof(&self) -> bool {
self.operator_ref.is_some()
}
pub fn has_fixture_proof(&self) -> bool {
self.fixture_ref.is_some()
&& self.expected_assertion.is_some()
&& self.fixture_digest.is_some()
}
pub fn has_external_receipt_proof(&self) -> bool {
self.external_receipt_ref.is_some()
&& self.external_receipt_digest.is_some()
&& self.source_adapter.is_some()
&& self.degradation_trust_note.is_some()
}
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct SupportAdmission {
pub claim_id: String,
pub previous_support_judgment_ref: String,
pub new_support_judgment_ref: String,
pub method: SupportAdmissionMethod,
pub admitted_support_state: SupportState,
pub rationale: String,
pub operator_ref: Option<String>,
pub proof_payload: Option<SupportProofPayload>,
pub proof_debt_waiver: Option<String>,
pub recorded_time: DateTime<Utc>,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct ContradictionRecord {
pub contradiction_id: String,
pub claim_refs: Vec<String>,
pub support_judgment_refs: Vec<String>,
pub source_span_refs: Vec<String>,
pub pattern: String,
pub rationale: String,
pub status: ContradictionStatus,
pub created_recorded_time: DateTime<Utc>,
}
impl ContradictionRecord {
pub fn new(left_claim_id: &str, right_claim_id: &str, pattern: &str, rationale: &str) -> Self {
let claim_refs = if left_claim_id <= right_claim_id {
vec![left_claim_id.to_string(), right_claim_id.to_string()]
} else {
vec![right_claim_id.to_string(), left_claim_id.to_string()]
};
Self {
contradiction_id: ids::contradiction_id(left_claim_id, right_claim_id, pattern),
claim_refs,
support_judgment_refs: Vec::new(),
source_span_refs: Vec::new(),
pattern: pattern.to_string(),
rationale: rationale.to_string(),
status: ContradictionStatus::Candidate,
created_recorded_time: Utc::now(),
}
}
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct ContradictionResolutionRecord {
pub contradiction_id: String,
pub previous_status: ContradictionStatus,
pub resolution: ContradictionResolution,
pub rationale: String,
pub affected_support_judgment_refs: Vec<String>,
pub supersession_receipt_refs: Vec<String>,
pub review_queue_refs: Vec<String>,
pub recorded_time: DateTime<Utc>,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct Supersession {
pub superseded_ref: String,
pub superseding_ref: String,
pub rationale: String,
pub recorded_time: DateTime<Utc>,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct Claim {
pub claim_id: String,
pub source_id: String,
pub span_id: String,
pub claim_text: String,
pub normalized_claim: String,
pub claim_type: String,
pub status: String,
pub support_judgment_ref: String,
pub evidence_bundle_ref: String,
pub proof_debt: Vec<ProofDebt>,
pub recommended_action: String,
pub rationale: String,
pub confidence: f64,
pub created_at: Option<DateTime<Utc>>,
pub metadata: serde_json::Value,
}
impl Claim {
pub fn new(source_id: &str, span_id: &str, claim_text: &str, claim_type: &str) -> Self {
let normalized = ids::normalize_text(claim_text);
Self {
claim_id: ids::claim_id(source_id, span_id, &normalized),
source_id: source_id.to_string(),
span_id: span_id.to_string(),
claim_text: claim_text.to_string(),
normalized_claim: normalized,
claim_type: claim_type.to_string(),
status: "pending".to_string(),
support_judgment_ref: String::new(),
evidence_bundle_ref: String::new(),
proof_debt: vec![ProofDebt::MissingSourceBasis],
recommended_action: "await_support_judgment".to_string(),
rationale: String::new(),
confidence: 0.0,
created_at: None,
metadata: serde_json::Value::Object(Default::default()),
}
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn support_state_is_strong() {
assert!(SupportState::Supported.is_strong());
assert!(SupportState::PartiallySupported.is_strong());
assert!(!SupportState::Unsupported.is_strong());
assert!(!SupportState::Contradicted.is_strong());
assert!(!SupportState::HeuristicOnly.is_strong());
assert!(!SupportState::Unknown.is_strong());
}
#[test]
fn evidence_bundle_new_has_id_prefix() {
let bundle = EvidenceBundle::new("clm_abc123");
assert!(bundle.evidence_bundle_id.starts_with("evb_"));
assert_eq!(bundle.claim_id, "clm_abc123");
}
#[test]
fn contradiction_record_new_is_ordered() {
let c1 = ContradictionRecord::new("clm_a", "clm_b", "exact", "texts contradict");
let c2 = ContradictionRecord::new("clm_b", "clm_a", "exact", "texts contradict");
assert_eq!(c1.contradiction_id, c2.contradiction_id);
assert_eq!(c1.claim_refs, c2.claim_refs);
}
#[test]
fn support_proof_payload_has_methods() {
let operator = SupportProofPayload {
operator_ref: Some("op-123".to_string()),
..Default::default()
};
assert!(operator.has_operator_proof());
let fixture = SupportProofPayload {
fixture_ref: Some("fix-456".to_string()),
expected_assertion: Some("claim is true".to_string()),
fixture_digest: Some("abc123".to_string()),
..Default::default()
};
assert!(fixture.has_fixture_proof());
}
#[test]
fn claim_new_has_deterministic_id() {
let c1 = Claim::new("src1", "sp1", "The sky is blue", "fact");
let c2 = Claim::new("src1", "sp1", "The sky is blue", "fact");
assert_eq!(c1.claim_id, c2.claim_id);
}
}