use super::common::{Description, LifecycleStatus, ObjectRef, ReviewRequirement};
use super::validation::{
require_declared_scope, require_min_len, require_non_empty, require_reviewed, require_some,
};
use crate::text::normalize_required_text;
use crate::{Confidence, CoreError, Id, Provenance, Result, ReviewStatus};
use serde::{Deserialize, Serialize};
#[derive(Clone, Copy, Debug, Deserialize, Eq, Hash, PartialEq, Serialize)]
#[serde(rename_all = "snake_case")]
pub enum EquivalenceKind {
StrictIdentity,
ContextualEquivalence,
ObservationalEquivalence,
BehavioralEquivalence,
SemanticNearEquivalence,
QuotientEquivalence,
}
#[derive(Clone, Debug, Deserialize, PartialEq, Serialize)]
#[serde(deny_unknown_fields)]
pub struct EquivalenceScope {
#[serde(default, skip_serializing_if = "Vec::is_empty")]
pub contexts: Vec<Id>,
#[serde(default, skip_serializing_if = "Vec::is_empty")]
pub valid_under_morphisms: Vec<Id>,
}
impl EquivalenceScope {
pub fn is_declared(&self) -> bool {
!self.contexts.is_empty() || !self.valid_under_morphisms.is_empty()
}
}
#[derive(Clone, Debug, Deserialize, PartialEq, Serialize)]
#[serde(deny_unknown_fields)]
pub struct EquivalenceCriterion {
pub description: String,
#[serde(default, skip_serializing_if = "Vec::is_empty")]
pub required_invariants: Vec<Id>,
#[serde(default, skip_serializing_if = "Vec::is_empty")]
pub ignored_distinctions: Vec<Description>,
}
impl EquivalenceCriterion {
pub fn new(description: impl Into<String>) -> Result<Self> {
Ok(Self {
description: normalize_required_text("criterion.description", description)?,
required_invariants: Vec::new(),
ignored_distinctions: Vec::new(),
})
}
}
#[derive(Clone, Debug, Deserialize, PartialEq, Serialize)]
#[serde(deny_unknown_fields)]
pub struct QuotientEffect {
#[serde(default, skip_serializing_if = "Vec::is_empty")]
pub lost_distinctions: Vec<Description>,
#[serde(default, skip_serializing_if = "Vec::is_empty")]
pub merged_cells: Vec<Id>,
#[serde(default, skip_serializing_if = "Vec::is_empty")]
pub affected_invariants: Vec<Id>,
#[serde(default, skip_serializing_if = "Vec::is_empty")]
pub affected_projections: Vec<Id>,
#[serde(default, skip_serializing_if = "Vec::is_empty")]
pub unresolved_obstructions: Vec<Id>,
}
#[derive(Clone, Debug, Deserialize, PartialEq, Serialize)]
#[serde(deny_unknown_fields)]
pub struct EquivalenceClaim {
pub id: Id,
pub subjects: Vec<ObjectRef>,
pub equivalence_kind: EquivalenceKind,
#[serde(skip_serializing_if = "Option::is_none")]
pub scope: Option<EquivalenceScope>,
#[serde(skip_serializing_if = "Option::is_none")]
pub criterion: Option<EquivalenceCriterion>,
#[serde(default, skip_serializing_if = "Vec::is_empty")]
pub witnesses: Vec<Id>,
#[serde(default, skip_serializing_if = "Vec::is_empty")]
pub counter_witnesses: Vec<Id>,
#[serde(skip_serializing_if = "Option::is_none")]
pub quotient_effect: Option<QuotientEffect>,
pub confidence: Confidence,
pub status: LifecycleStatus,
pub provenance: Provenance,
#[serde(skip_serializing_if = "Option::is_none")]
pub review: Option<ReviewRequirement>,
}
impl EquivalenceClaim {
pub fn candidate(
id: Id,
subjects: Vec<ObjectRef>,
equivalence_kind: EquivalenceKind,
confidence: Confidence,
provenance: Provenance,
) -> Self {
Self {
id,
subjects,
equivalence_kind,
scope: None,
criterion: None,
witnesses: Vec::new(),
counter_witnesses: Vec::new(),
quotient_effect: None,
confidence,
status: LifecycleStatus::Candidate,
provenance,
review: None,
}
}
pub fn validate_acceptance(&self) -> Result<()> {
require_min_len("subjects", self.subjects.len(), 2)?;
require_declared_scope(self.scope.as_ref().map(EquivalenceScope::is_declared))?;
require_some("criterion", self.criterion.as_ref())?;
require_non_empty("witnesses", &self.witnesses)?;
let quotient_effect = require_some("quotient_effect", self.quotient_effect.as_ref())?;
if !quotient_effect.unresolved_obstructions.is_empty() {
return Err(CoreError::malformed_field(
"quotient_effect.unresolved_obstructions",
"accepted equivalence must not have unresolved obstructions",
));
}
if self.equivalence_kind == EquivalenceKind::StrictIdentity
&& !self.counter_witnesses.is_empty()
{
return Err(CoreError::malformed_field(
"counter_witnesses",
"strict identity cannot be accepted with counter witnesses",
));
}
if self.provenance.source.kind == crate::SourceKind::Ai
&& self.provenance.review_status != ReviewStatus::Accepted
{
return Err(CoreError::malformed_field(
"provenance.review_status",
"AI-generated equivalence requires explicit accepted review",
));
}
require_reviewed(self.review.as_ref(), "review")?;
Ok(())
}
pub fn can_merge_equivalence(&self) -> bool {
self.status.is_accepted() && self.validate_acceptance().is_ok()
}
}