use crate::{Confidence, CoreError, Id, Result, ReviewRequirement, ReviewStatus};
use serde::de::{self, MapAccess, Visitor};
use serde::{Deserialize, Deserializer, Serialize};
use std::collections::BTreeMap;
use std::fmt;
#[derive(Clone, Debug, Eq, Hash, Ord, PartialEq, PartialOrd, Serialize)]
#[serde(tag = "kind", content = "id", rename_all = "camelCase")]
pub enum ParticipantRef {
Cell(Id),
Complex(Id),
Context(Id),
Projection(Id),
Claim(Id),
Evidence(Id),
Invariant(Id),
Obstruction(Id),
CompletionCandidate(Id),
}
impl ParticipantRef {
pub fn from_compact_id(id: Id) -> Self {
let value = id.as_str();
if value.starts_with("complex:") {
Self::Complex(id)
} else if value.starts_with("ctx:") || value.starts_with("context:") {
Self::Context(id)
} else if value.starts_with("projection:") {
Self::Projection(id)
} else if value.starts_with("claim:") {
Self::Claim(id)
} else if value.starts_with("evidence:") {
Self::Evidence(id)
} else if value.starts_with("invariant:") {
Self::Invariant(id)
} else if value.starts_with("obstruction:") {
Self::Obstruction(id)
} else if value.starts_with("completion:")
|| value.starts_with("completion_candidate:")
|| value.starts_with("candidate:")
{
Self::CompletionCandidate(id)
} else {
Self::Cell(id)
}
}
#[must_use]
pub fn id(&self) -> &Id {
match self {
Self::Cell(id)
| Self::Complex(id)
| Self::Context(id)
| Self::Projection(id)
| Self::Claim(id)
| Self::Evidence(id)
| Self::Invariant(id)
| Self::Obstruction(id)
| Self::CompletionCandidate(id) => id,
}
}
}
impl<'de> Deserialize<'de> for ParticipantRef {
fn deserialize<D>(deserializer: D) -> std::result::Result<Self, D::Error>
where
D: Deserializer<'de>,
{
deserializer.deserialize_any(ParticipantRefVisitor)
}
}
struct ParticipantRefVisitor;
impl<'de> Visitor<'de> for ParticipantRefVisitor {
type Value = ParticipantRef;
fn expecting(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
formatter.write_str("a compact participant id string or {kind, id} object")
}
fn visit_str<E>(self, value: &str) -> std::result::Result<Self::Value, E>
where
E: de::Error,
{
Id::new(value)
.map(ParticipantRef::from_compact_id)
.map_err(E::custom)
}
fn visit_string<E>(self, value: String) -> std::result::Result<Self::Value, E>
where
E: de::Error,
{
self.visit_str(&value)
}
fn visit_map<M>(self, mut access: M) -> std::result::Result<Self::Value, M::Error>
where
M: MapAccess<'de>,
{
let mut kind: Option<String> = None;
let mut id: Option<Id> = None;
while let Some(key) = access.next_key::<String>()? {
match key.as_str() {
"kind" => kind = Some(access.next_value()?),
"id" => id = Some(access.next_value()?),
unknown => {
return Err(de::Error::unknown_field(unknown, &["kind", "id"]));
}
}
}
let kind = kind.ok_or_else(|| de::Error::missing_field("kind"))?;
let id = id.ok_or_else(|| de::Error::missing_field("id"))?;
match kind.as_str() {
"cell" => Ok(ParticipantRef::Cell(id)),
"complex" => Ok(ParticipantRef::Complex(id)),
"context" => Ok(ParticipantRef::Context(id)),
"projection" => Ok(ParticipantRef::Projection(id)),
"claim" => Ok(ParticipantRef::Claim(id)),
"evidence" => Ok(ParticipantRef::Evidence(id)),
"invariant" => Ok(ParticipantRef::Invariant(id)),
"obstruction" => Ok(ParticipantRef::Obstruction(id)),
"completionCandidate" => Ok(ParticipantRef::CompletionCandidate(id)),
unknown => Err(de::Error::unknown_variant(
unknown,
&[
"cell",
"complex",
"context",
"projection",
"claim",
"evidence",
"invariant",
"obstruction",
"completionCandidate",
],
)),
}
}
}
#[derive(Clone, Debug, Deserialize, Eq, Hash, Ord, PartialEq, PartialOrd, Serialize)]
#[serde(rename_all = "camelCase", deny_unknown_fields)]
pub struct CorrespondenceParticipant {
pub role: String,
#[serde(rename = "ref")]
pub participant: ParticipantRef,
}
impl CorrespondenceParticipant {
pub fn new(role: impl Into<String>, participant: ParticipantRef) -> Result<Self> {
Ok(Self {
role: required_text("role", role)?,
participant,
})
}
}
#[derive(Clone, Copy, Debug, Deserialize, Eq, Hash, Ord, PartialEq, PartialOrd, Serialize)]
pub enum CorrespondenceKind {
ExactIdentity,
SurfaceOverlap,
SemanticOverlap,
StructuralOverlap,
ConstraintOverlap,
EvidenceOverlap,
ContextualOverlap,
CausalOverlap,
ProjectionOverlap,
Refinement,
Abstraction,
Conflict,
Synergy,
Obstructed,
}
#[derive(Clone, Copy, Debug, Deserialize, Eq, Hash, Ord, PartialEq, PartialOrd, Serialize)]
pub enum CorrespondencePolarity {
Agreeing,
Conflicting,
Refining,
Projecting,
Ambiguous,
Unknown,
}
#[derive(Clone, Copy, Debug, Deserialize, Eq, Hash, Ord, PartialEq, PartialOrd, Serialize)]
pub enum OverlapWitnessKind {
FeatureSet,
PredicateSet,
NormalizedClaim,
Subgraph,
Subcomplex,
ConstraintSet,
EvidenceSet,
Boundary,
ProjectionTrace,
CausalPattern,
ContextRestriction,
}
impl OverlapWitnessKind {
#[must_use]
pub fn supports_accepted_semantic_overlap(self) -> bool {
matches!(
self,
Self::NormalizedClaim | Self::PredicateSet | Self::FeatureSet
)
}
}
#[derive(Clone, Copy, Debug, Deserialize, Eq, Hash, Ord, PartialEq, PartialOrd, Serialize)]
pub enum DifferenceKind {
AdditionalDetail,
MissingDetail,
TypeMismatch,
PredicateMismatch,
ModalityMismatch,
ContextMismatch,
EvidenceMismatch,
ConfidenceMismatch,
TemporalMismatch,
InvariantMismatch,
Contradiction,
ProjectionLoss,
}
#[derive(Clone, Copy, Debug, Deserialize, Eq, Hash, Ord, PartialEq, PartialOrd, Serialize)]
#[serde(rename_all = "snake_case")]
pub enum DifferenceSeverity {
Informational,
Minor,
Major,
Blocking,
}
#[derive(Clone, Debug, Deserialize, PartialEq, Serialize)]
#[serde(tag = "kind", content = "value", rename_all = "camelCase")]
pub enum SharedStructure {
FeatureSet(Vec<Feature>),
PredicateSet(Vec<Predicate>),
NormalizedClaim(NormalizedClaim),
Subgraph(SubgraphPattern),
Subcomplex(SubcomplexPattern),
ConstraintSet(Vec<Id>),
EvidenceSet(Vec<Id>),
Boundary(BoundaryPattern),
ProjectionTrace(ProjectionTrace),
CausalPattern(CausalPattern),
ContextRestriction(ContextRestriction),
}
#[derive(Clone, Debug, Deserialize, PartialEq, Serialize)]
#[serde(tag = "kind", content = "value", rename_all = "camelCase")]
pub enum DifferingStructure {
AdditionalDetail(BTreeMap<String, String>),
MissingDetail(BTreeMap<String, String>),
TypeMismatch(BTreeMap<String, String>),
PredicateMismatch(BTreeMap<String, String>),
ModalityMismatch(BTreeMap<String, String>),
ContextMismatch(BTreeMap<String, String>),
EvidenceMismatch(Vec<Id>),
ConfidenceMismatch(BTreeMap<String, Confidence>),
TemporalMismatch(BTreeMap<String, String>),
InvariantMismatch(Vec<InvariantCheckResult>),
Contradiction(BTreeMap<String, String>),
ProjectionLoss(ProjectionLoss),
}
#[derive(Clone, Debug, Deserialize, Eq, Hash, Ord, PartialEq, PartialOrd, Serialize)]
#[serde(rename_all = "camelCase", deny_unknown_fields)]
pub struct Feature {
pub key: String,
pub value: String,
}
#[derive(Clone, Debug, Deserialize, Eq, Hash, Ord, PartialEq, PartialOrd, Serialize)]
#[serde(rename_all = "camelCase", deny_unknown_fields)]
pub struct Predicate {
pub subject: String,
pub relation: String,
pub object: String,
}
#[derive(Clone, Debug, Deserialize, Eq, Hash, Ord, PartialEq, PartialOrd, Serialize)]
#[serde(rename_all = "camelCase", deny_unknown_fields)]
pub struct NormalizedClaim {
pub subject: String,
pub relation: String,
pub object: String,
#[serde(skip_serializing_if = "Option::is_none")]
pub modality: Option<String>,
#[serde(skip_serializing_if = "Option::is_none")]
pub temporal_scope: Option<String>,
}
#[derive(Clone, Debug, Default, Deserialize, Eq, Hash, Ord, PartialEq, PartialOrd, Serialize)]
#[serde(rename_all = "camelCase", deny_unknown_fields)]
pub struct SubgraphPattern {
#[serde(default, skip_serializing_if = "Vec::is_empty")]
pub node_ids: Vec<Id>,
#[serde(default, skip_serializing_if = "Vec::is_empty")]
pub edge_ids: Vec<Id>,
}
#[derive(Clone, Debug, Default, Deserialize, Eq, Hash, Ord, PartialEq, PartialOrd, Serialize)]
#[serde(rename_all = "camelCase", deny_unknown_fields)]
pub struct SubcomplexPattern {
#[serde(default, skip_serializing_if = "Vec::is_empty")]
pub cell_ids: Vec<Id>,
#[serde(default, skip_serializing_if = "Vec::is_empty")]
pub incidence_ids: Vec<Id>,
}
#[derive(Clone, Debug, Default, Deserialize, Eq, Hash, Ord, PartialEq, PartialOrd, Serialize)]
#[serde(rename_all = "camelCase", deny_unknown_fields)]
pub struct BoundaryPattern {
#[serde(default, skip_serializing_if = "Vec::is_empty")]
pub boundary_cell_ids: Vec<Id>,
}
#[derive(Clone, Debug, Default, Deserialize, Eq, Hash, Ord, PartialEq, PartialOrd, Serialize)]
#[serde(rename_all = "camelCase", deny_unknown_fields)]
pub struct ProjectionTrace {
#[serde(default, skip_serializing_if = "Vec::is_empty")]
pub source_ids: Vec<Id>,
#[serde(default, skip_serializing_if = "Vec::is_empty")]
pub projection_ids: Vec<Id>,
}
#[derive(Clone, Debug, Default, Deserialize, Eq, Hash, Ord, PartialEq, PartialOrd, Serialize)]
#[serde(rename_all = "camelCase", deny_unknown_fields)]
pub struct CausalPattern {
#[serde(default, skip_serializing_if = "Vec::is_empty")]
pub cause_ids: Vec<Id>,
#[serde(default, skip_serializing_if = "Vec::is_empty")]
pub effect_ids: Vec<Id>,
#[serde(default, skip_serializing_if = "Vec::is_empty")]
pub path_ids: Vec<Id>,
}
#[derive(Clone, Debug, Default, Deserialize, Eq, Hash, Ord, PartialEq, PartialOrd, Serialize)]
#[serde(rename_all = "camelCase", deny_unknown_fields)]
pub struct ContextRestriction {
#[serde(default, skip_serializing_if = "Vec::is_empty")]
pub source_context_ids: Vec<Id>,
#[serde(skip_serializing_if = "Option::is_none")]
pub target_context_id: Option<Id>,
#[serde(default, skip_serializing_if = "Vec::is_empty")]
pub retained_element_ids: Vec<Id>,
}
#[derive(Clone, Debug, Deserialize, Eq, Hash, Ord, PartialEq, PartialOrd, Serialize)]
#[serde(rename_all = "camelCase", deny_unknown_fields)]
pub struct ParticipantMapping {
pub participant: Id,
pub path: String,
}
#[derive(Clone, Debug, Default, Deserialize, Eq, Hash, Ord, PartialEq, PartialOrd, Serialize)]
#[serde(rename_all = "camelCase", deny_unknown_fields)]
pub struct Scope {
#[serde(default, skip_serializing_if = "Vec::is_empty")]
pub structure_ids: Vec<Id>,
#[serde(skip_serializing_if = "Option::is_none")]
pub boundary: Option<String>,
}
#[derive(Clone, Debug, Deserialize, PartialEq, Serialize)]
#[serde(rename_all = "camelCase", deny_unknown_fields)]
pub struct OverlapWitness {
pub id: Id,
pub witness_kind: OverlapWitnessKind,
pub shared_structure: SharedStructure,
#[serde(default, skip_serializing_if = "Vec::is_empty")]
pub participant_mappings: Vec<ParticipantMapping>,
#[serde(default, skip_serializing_if = "Scope::is_empty")]
pub scope: Scope,
pub context: Id,
#[serde(default, skip_serializing_if = "Vec::is_empty")]
pub evidence: Vec<Id>,
pub confidence: Confidence,
pub status: ReviewStatus,
}
impl OverlapWitness {
#[must_use]
pub fn supports_accepted_semantic_overlap(&self) -> bool {
self.witness_kind.supports_accepted_semantic_overlap()
}
}
#[derive(Clone, Debug, Deserialize, PartialEq, Serialize)]
#[serde(rename_all = "camelCase", deny_unknown_fields)]
pub struct DifferenceWitness {
pub id: Id,
pub difference_kind: DifferenceKind,
pub differing_structure: DifferingStructure,
#[serde(default, skip_serializing_if = "Vec::is_empty")]
pub participant_mappings: Vec<ParticipantMapping>,
pub severity: DifferenceSeverity,
pub context: Id,
#[serde(default, skip_serializing_if = "Vec::is_empty")]
pub evidence: Vec<Id>,
pub confidence: Confidence,
pub status: ReviewStatus,
}
#[derive(Clone, Debug, Deserialize, Eq, Hash, Ord, PartialEq, PartialOrd, Serialize)]
#[serde(rename_all = "camelCase", deny_unknown_fields)]
pub struct InvariantCheckResult {
pub invariant: Id,
pub result: String,
#[serde(skip_serializing_if = "Option::is_none")]
pub detail: Option<String>,
}
#[derive(Clone, Debug, Default, Deserialize, Eq, Hash, Ord, PartialEq, PartialOrd, Serialize)]
#[serde(rename_all = "camelCase", deny_unknown_fields)]
pub struct PreservationReport {
#[serde(default, skip_serializing_if = "Vec::is_empty")]
pub preserved_invariants: Vec<Id>,
#[serde(default, skip_serializing_if = "Vec::is_empty")]
pub preserved_structures: Vec<Id>,
#[serde(skip_serializing_if = "Option::is_none")]
pub summary: Option<String>,
}
#[derive(Clone, Debug, Default, Deserialize, Eq, Hash, Ord, PartialEq, PartialOrd, Serialize)]
#[serde(rename_all = "camelCase", deny_unknown_fields)]
pub struct ProjectionLoss {
#[serde(default, skip_serializing_if = "Vec::is_empty")]
pub omitted_overlap_witnesses: Vec<Id>,
#[serde(default, skip_serializing_if = "Vec::is_empty")]
pub omitted_difference_witnesses: Vec<Id>,
#[serde(default, skip_serializing_if = "Vec::is_empty")]
pub omitted_evidence: Vec<Id>,
#[serde(default, skip_serializing_if = "Vec::is_empty")]
pub omitted_contexts: Vec<Id>,
#[serde(default, skip_serializing_if = "Vec::is_empty")]
pub collapsed_statuses: Vec<ReviewStatusCollapse>,
}
#[derive(Clone, Debug, Deserialize, Eq, Hash, Ord, PartialEq, PartialOrd, Serialize)]
#[serde(rename_all = "camelCase", deny_unknown_fields)]
pub struct ReviewStatusCollapse {
pub source_id: Id,
pub from: ReviewStatus,
pub to: ReviewStatus,
}
#[derive(Clone, Copy, Debug, Deserialize, Eq, Hash, Ord, PartialEq, PartialOrd, Serialize)]
#[serde(rename_all = "snake_case")]
pub enum CorrespondenceValidationCode {
MissingParticipants,
AcceptedMissingEvidence,
ConflictMissingSharedStructure,
SemanticOverlapMissingExplicitWitness,
GluingSuccessMissingPreservationReport,
BlockingDifferenceSilentMerge,
}
#[derive(Clone, Debug, Deserialize, Eq, Hash, Ord, PartialEq, PartialOrd, Serialize)]
#[serde(rename_all = "camelCase", deny_unknown_fields)]
pub struct CorrespondenceValidationFinding {
pub code: CorrespondenceValidationCode,
pub field: String,
pub reason: String,
}
impl CorrespondenceValidationFinding {
fn new(
code: CorrespondenceValidationCode,
field: impl Into<String>,
reason: impl Into<String>,
) -> Self {
Self {
code,
field: field.into(),
reason: reason.into(),
}
}
}
#[derive(Clone, Debug, Deserialize, Eq, PartialEq, Serialize)]
#[serde(rename_all = "camelCase", deny_unknown_fields)]
pub struct CorrespondenceValidationReport {
pub correspondence_id: Id,
#[serde(default, skip_serializing_if = "Vec::is_empty")]
pub findings: Vec<CorrespondenceValidationFinding>,
}
impl CorrespondenceValidationReport {
#[must_use]
pub fn is_valid(&self) -> bool {
self.findings.is_empty()
}
}
#[derive(Clone, Debug, Deserialize, PartialEq, Serialize)]
#[serde(tag = "kind", rename_all = "snake_case")]
pub enum GluingResult {
Success {
#[serde(rename = "mergedComplex")]
merged_complex: Id,
#[serde(rename = "preservationReport")]
preservation_report: PreservationReport,
},
Candidate {
#[serde(rename = "completionCandidate")]
completion_candidate: Id,
#[serde(rename = "requiredReview")]
required_review: ReviewRequirement,
},
Failure {
obstruction: Id,
},
}
#[derive(Clone, Debug, Deserialize, PartialEq, Serialize)]
#[serde(rename_all = "camelCase", deny_unknown_fields)]
pub struct GluingAttempt {
pub id: Id,
#[serde(default, skip_serializing_if = "Vec::is_empty")]
pub participants: Vec<ParticipantRef>,
#[serde(default, skip_serializing_if = "Vec::is_empty")]
pub overlap_witnesses: Vec<Id>,
#[serde(default, skip_serializing_if = "Vec::is_empty")]
pub difference_witnesses: Vec<Id>,
pub context: Id,
#[serde(default, skip_serializing_if = "Vec::is_empty")]
pub invariant_checks: Vec<InvariantCheckResult>,
#[serde(default, skip_serializing_if = "PreservationReport::is_empty")]
pub preservation_report: PreservationReport,
pub result: GluingResult,
#[serde(default, skip_serializing_if = "Vec::is_empty")]
pub evidence: Vec<Id>,
pub confidence: Confidence,
pub status: ReviewStatus,
#[serde(skip_serializing_if = "Option::is_none")]
pub override_review: Option<ReviewRequirement>,
}
#[derive(Clone, Debug, Deserialize, PartialEq, Serialize)]
#[serde(rename_all = "camelCase", deny_unknown_fields)]
pub struct CorrespondenceCell {
pub id: Id,
pub participants: Vec<CorrespondenceParticipant>,
pub correspondence_kind: CorrespondenceKind,
pub polarity: CorrespondencePolarity,
#[serde(default, skip_serializing_if = "Vec::is_empty")]
pub overlap_witnesses: Vec<OverlapWitness>,
#[serde(default, skip_serializing_if = "Vec::is_empty")]
pub difference_witnesses: Vec<DifferenceWitness>,
pub context: Id,
#[serde(default, skip_serializing_if = "Vec::is_empty")]
pub evidence: Vec<Id>,
pub provenance: Id,
pub confidence: Confidence,
pub review_status: ReviewStatus,
#[serde(skip_serializing_if = "Option::is_none")]
pub gluing: Option<GluingAttempt>,
}
impl CorrespondenceCell {
#[must_use]
pub fn validate_report(&self) -> CorrespondenceValidationReport {
let mut findings = Vec::new();
if self.participants.len() < 2 {
findings.push(CorrespondenceValidationFinding::new(
CorrespondenceValidationCode::MissingParticipants,
"participants",
"correspondence requires at least two participants",
));
}
if self.review_status.is_accepted() && self.evidence.is_empty() {
findings.push(CorrespondenceValidationFinding::new(
CorrespondenceValidationCode::AcceptedMissingEvidence,
"evidence",
"accepted correspondence requires supporting evidence",
));
}
if matches!(self.polarity, CorrespondencePolarity::Conflicting)
&& self.overlap_witnesses.is_empty()
{
findings.push(CorrespondenceValidationFinding::new(
CorrespondenceValidationCode::ConflictMissingSharedStructure,
"overlap_witnesses",
"conflicting correspondence requires explicit shared structure",
));
}
if matches!(
self.correspondence_kind,
CorrespondenceKind::SemanticOverlap
) && self.review_status.is_accepted()
&& !self
.overlap_witnesses
.iter()
.any(OverlapWitness::supports_accepted_semantic_overlap)
{
findings.push(CorrespondenceValidationFinding::new(
CorrespondenceValidationCode::SemanticOverlapMissingExplicitWitness,
"overlap_witnesses",
"accepted semantic overlap requires normalized claim, predicate set, or feature set witness",
));
}
if let Some(gluing) = &self.gluing {
findings.extend(gluing.validation_findings(&self.difference_witnesses));
}
CorrespondenceValidationReport {
correspondence_id: self.id.clone(),
findings,
}
}
pub fn validate(&self) -> Result<()> {
let report = self.validate_report();
if let Some(finding) = report.findings.first() {
return Err(malformed_field(&finding.field, finding.reason.clone()));
}
Ok(())
}
}
impl GluingAttempt {
fn validation_findings(
&self,
differences: &[DifferenceWitness],
) -> Vec<CorrespondenceValidationFinding> {
let mut findings = Vec::new();
if let GluingResult::Success {
preservation_report,
..
} = &self.result
{
if preservation_report.is_empty() && self.preservation_report.is_empty() {
findings.push(CorrespondenceValidationFinding::new(
CorrespondenceValidationCode::GluingSuccessMissingPreservationReport,
"preservation_report",
"gluing success requires a preservation report",
));
}
if self.override_review.is_none()
&& differences
.iter()
.any(|difference| matches!(difference.severity, DifferenceSeverity::Blocking))
{
findings.push(CorrespondenceValidationFinding::new(
CorrespondenceValidationCode::BlockingDifferenceSilentMerge,
"result",
"blocking difference prevents gluing success without explicit override review",
));
}
}
findings
}
pub fn validate_with_differences(&self, differences: &[DifferenceWitness]) -> Result<()> {
let findings = self.validation_findings(differences);
if let Some(finding) = findings.first() {
return Err(malformed_field(&finding.field, finding.reason.clone()));
}
Ok(())
}
}
impl Scope {
fn is_empty(&self) -> bool {
self.structure_ids.is_empty() && self.boundary.is_none()
}
}
impl PreservationReport {
fn is_empty(&self) -> bool {
self.preserved_invariants.is_empty()
&& self.preserved_structures.is_empty()
&& self.summary.is_none()
}
}
fn required_text(field: impl Into<String>, value: impl Into<String>) -> Result<String> {
let raw = value.into();
let normalized = raw.trim().to_owned();
if normalized.is_empty() {
return Err(malformed_field(
field,
"value must not be empty after trimming",
));
}
Ok(normalized)
}
fn malformed_field(field: impl Into<String>, reason: impl Into<String>) -> CoreError {
CoreError::MalformedField {
field: field.into(),
reason: reason.into(),
}
}