Skip to main content

higher_graphen_core/
correspondence.rs

1use crate::{Confidence, CoreError, Id, Result, ReviewRequirement, ReviewStatus};
2use serde::de::{self, MapAccess, Visitor};
3use serde::{Deserialize, Deserializer, Serialize};
4use std::collections::BTreeMap;
5use std::fmt;
6
7/// Reference to a structure that participates in a correspondence.
8#[derive(Clone, Debug, Eq, Hash, Ord, PartialEq, PartialOrd, Serialize)]
9#[serde(tag = "kind", content = "id", rename_all = "camelCase")]
10pub enum ParticipantRef {
11    /// Cell participant.
12    Cell(Id),
13    /// Complex participant.
14    Complex(Id),
15    /// Context participant.
16    Context(Id),
17    /// Projection participant.
18    Projection(Id),
19    /// Claim participant.
20    Claim(Id),
21    /// Evidence participant.
22    Evidence(Id),
23    /// Invariant participant.
24    Invariant(Id),
25    /// Obstruction participant.
26    Obstruction(Id),
27    /// Completion candidate participant.
28    CompletionCandidate(Id),
29}
30
31impl ParticipantRef {
32    /// Creates a participant reference from a compact prefixed identifier.
33    pub fn from_compact_id(id: Id) -> Self {
34        let value = id.as_str();
35        if value.starts_with("complex:") {
36            Self::Complex(id)
37        } else if value.starts_with("ctx:") || value.starts_with("context:") {
38            Self::Context(id)
39        } else if value.starts_with("projection:") {
40            Self::Projection(id)
41        } else if value.starts_with("claim:") {
42            Self::Claim(id)
43        } else if value.starts_with("evidence:") {
44            Self::Evidence(id)
45        } else if value.starts_with("invariant:") {
46            Self::Invariant(id)
47        } else if value.starts_with("obstruction:") {
48            Self::Obstruction(id)
49        } else if value.starts_with("completion:")
50            || value.starts_with("completion_candidate:")
51            || value.starts_with("candidate:")
52        {
53            Self::CompletionCandidate(id)
54        } else {
55            Self::Cell(id)
56        }
57    }
58
59    /// Returns the participant identifier regardless of participant kind.
60    #[must_use]
61    pub fn id(&self) -> &Id {
62        match self {
63            Self::Cell(id)
64            | Self::Complex(id)
65            | Self::Context(id)
66            | Self::Projection(id)
67            | Self::Claim(id)
68            | Self::Evidence(id)
69            | Self::Invariant(id)
70            | Self::Obstruction(id)
71            | Self::CompletionCandidate(id) => id,
72        }
73    }
74}
75
76impl<'de> Deserialize<'de> for ParticipantRef {
77    fn deserialize<D>(deserializer: D) -> std::result::Result<Self, D::Error>
78    where
79        D: Deserializer<'de>,
80    {
81        deserializer.deserialize_any(ParticipantRefVisitor)
82    }
83}
84
85struct ParticipantRefVisitor;
86
87impl<'de> Visitor<'de> for ParticipantRefVisitor {
88    type Value = ParticipantRef;
89
90    fn expecting(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
91        formatter.write_str("a compact participant id string or {kind, id} object")
92    }
93
94    fn visit_str<E>(self, value: &str) -> std::result::Result<Self::Value, E>
95    where
96        E: de::Error,
97    {
98        Id::new(value)
99            .map(ParticipantRef::from_compact_id)
100            .map_err(E::custom)
101    }
102
103    fn visit_string<E>(self, value: String) -> std::result::Result<Self::Value, E>
104    where
105        E: de::Error,
106    {
107        self.visit_str(&value)
108    }
109
110    fn visit_map<M>(self, mut access: M) -> std::result::Result<Self::Value, M::Error>
111    where
112        M: MapAccess<'de>,
113    {
114        let mut kind: Option<String> = None;
115        let mut id: Option<Id> = None;
116
117        while let Some(key) = access.next_key::<String>()? {
118            match key.as_str() {
119                "kind" => kind = Some(access.next_value()?),
120                "id" => id = Some(access.next_value()?),
121                unknown => {
122                    return Err(de::Error::unknown_field(unknown, &["kind", "id"]));
123                }
124            }
125        }
126
127        let kind = kind.ok_or_else(|| de::Error::missing_field("kind"))?;
128        let id = id.ok_or_else(|| de::Error::missing_field("id"))?;
129
130        match kind.as_str() {
131            "cell" => Ok(ParticipantRef::Cell(id)),
132            "complex" => Ok(ParticipantRef::Complex(id)),
133            "context" => Ok(ParticipantRef::Context(id)),
134            "projection" => Ok(ParticipantRef::Projection(id)),
135            "claim" => Ok(ParticipantRef::Claim(id)),
136            "evidence" => Ok(ParticipantRef::Evidence(id)),
137            "invariant" => Ok(ParticipantRef::Invariant(id)),
138            "obstruction" => Ok(ParticipantRef::Obstruction(id)),
139            "completionCandidate" => Ok(ParticipantRef::CompletionCandidate(id)),
140            unknown => Err(de::Error::unknown_variant(
141                unknown,
142                &[
143                    "cell",
144                    "complex",
145                    "context",
146                    "projection",
147                    "claim",
148                    "evidence",
149                    "invariant",
150                    "obstruction",
151                    "completionCandidate",
152                ],
153            )),
154        }
155    }
156}
157
158/// A role-labeled participant entry in a correspondence cell.
159#[derive(Clone, Debug, Deserialize, Eq, Hash, Ord, PartialEq, PartialOrd, Serialize)]
160#[serde(rename_all = "camelCase", deny_unknown_fields)]
161pub struct CorrespondenceParticipant {
162    /// Participant role within this correspondence.
163    pub role: String,
164    /// Referenced participant.
165    #[serde(rename = "ref")]
166    pub participant: ParticipantRef,
167}
168
169impl CorrespondenceParticipant {
170    /// Creates a role-labeled participant.
171    pub fn new(role: impl Into<String>, participant: ParticipantRef) -> Result<Self> {
172        Ok(Self {
173            role: required_text("role", role)?,
174            participant,
175        })
176    }
177}
178
179/// Category of relationship represented by a correspondence cell.
180#[derive(Clone, Copy, Debug, Deserialize, Eq, Hash, Ord, PartialEq, PartialOrd, Serialize)]
181pub enum CorrespondenceKind {
182    /// Same target or normalized representation.
183    ExactIdentity,
184    /// Surface fields such as strings, labels, or tags overlap.
185    SurfaceOverlap,
186    /// Meaning overlaps even when representation differs.
187    SemanticOverlap,
188    /// Structural pattern such as a subgraph or subcomplex overlaps.
189    StructuralOverlap,
190    /// Same invariant or constraint area is involved.
191    ConstraintOverlap,
192    /// Same evidence or provenance supports the participants.
193    EvidenceOverlap,
194    /// Contexts share a local region.
195    ContextualOverlap,
196    /// Same cause, effect, or causal path is involved.
197    CausalOverlap,
198    /// Projections derive from the same source structure.
199    ProjectionOverlap,
200    /// One participant details another.
201    Refinement,
202    /// One participant abstracts another.
203    Abstraction,
204    /// Shared region exists but claims, constraints, or states conflict.
205    Conflict,
206    /// Meaning emerges only from the participant combination.
207    Synergy,
208    /// Correspondence exists but gluing or integration is blocked.
209    Obstructed,
210}
211
212/// Directional or truth-status polarity of a correspondence.
213#[derive(Clone, Copy, Debug, Deserialize, Eq, Hash, Ord, PartialEq, PartialOrd, Serialize)]
214pub enum CorrespondencePolarity {
215    /// Participants agree over the shared structure.
216    Agreeing,
217    /// Participants conflict over the shared structure.
218    Conflicting,
219    /// One participant refines another.
220    Refining,
221    /// One participant is projected from another.
222    Projecting,
223    /// Polarity is ambiguous.
224    Ambiguous,
225    /// Polarity has not been determined.
226    Unknown,
227}
228
229/// Kind of overlap witness.
230#[derive(Clone, Copy, Debug, Deserialize, Eq, Hash, Ord, PartialEq, PartialOrd, Serialize)]
231pub enum OverlapWitnessKind {
232    /// Shared features.
233    FeatureSet,
234    /// Shared predicates.
235    PredicateSet,
236    /// Shared normalized claim.
237    NormalizedClaim,
238    /// Shared subgraph pattern.
239    Subgraph,
240    /// Shared subcomplex pattern.
241    Subcomplex,
242    /// Shared constraints.
243    ConstraintSet,
244    /// Shared evidence references.
245    EvidenceSet,
246    /// Shared boundary.
247    Boundary,
248    /// Shared projection trace.
249    ProjectionTrace,
250    /// Shared causal pattern.
251    CausalPattern,
252    /// Shared context restriction.
253    ContextRestriction,
254}
255
256impl OverlapWitnessKind {
257    /// Returns the stable serde discriminant string for this witness kind.
258    #[must_use]
259    pub fn kind(&self) -> &'static str {
260        match self {
261            Self::FeatureSet => "FeatureSet",
262            Self::PredicateSet => "PredicateSet",
263            Self::NormalizedClaim => "NormalizedClaim",
264            Self::Subgraph => "Subgraph",
265            Self::Subcomplex => "Subcomplex",
266            Self::ConstraintSet => "ConstraintSet",
267            Self::EvidenceSet => "EvidenceSet",
268            Self::Boundary => "Boundary",
269            Self::ProjectionTrace => "ProjectionTrace",
270            Self::CausalPattern => "CausalPattern",
271            Self::ContextRestriction => "ContextRestriction",
272        }
273    }
274
275    /// Returns true when this witness kind is explicit enough to support accepted semantic overlap.
276    #[must_use]
277    pub fn supports_accepted_semantic_overlap(self) -> bool {
278        matches!(
279            self,
280            Self::NormalizedClaim | Self::PredicateSet | Self::FeatureSet
281        )
282    }
283}
284
285/// Difference category inside a correspondence.
286#[derive(Clone, Copy, Debug, Deserialize, Eq, Hash, Ord, PartialEq, PartialOrd, Serialize)]
287pub enum DifferenceKind {
288    /// One participant contains extra detail.
289    AdditionalDetail,
290    /// One participant lacks detail present in another.
291    MissingDetail,
292    /// Types differ.
293    TypeMismatch,
294    /// Predicates differ.
295    PredicateMismatch,
296    /// Modalities such as observed, required, or forbidden differ.
297    ModalityMismatch,
298    /// Contexts differ or are incompatible.
299    ContextMismatch,
300    /// Evidence differs.
301    EvidenceMismatch,
302    /// Confidence differs materially.
303    ConfidenceMismatch,
304    /// Time or validity interval differs.
305    TemporalMismatch,
306    /// Invariant satisfaction differs.
307    InvariantMismatch,
308    /// Participants contradict each other.
309    Contradiction,
310    /// Projection omitted or collapsed information.
311    ProjectionLoss,
312}
313
314/// Severity of a difference witness.
315#[derive(Clone, Copy, Debug, Deserialize, Eq, Hash, Ord, PartialEq, PartialOrd, Serialize)]
316#[serde(rename_all = "snake_case")]
317pub enum DifferenceSeverity {
318    /// Difference is informational only.
319    Informational,
320    /// Difference is minor.
321    Minor,
322    /// Difference is major.
323    Major,
324    /// Difference blocks silent gluing or merge.
325    Blocking,
326}
327
328impl DifferenceSeverity {
329    /// Returns the stable serde discriminant string for this severity.
330    #[must_use]
331    pub fn kind(&self) -> &'static str {
332        match self {
333            Self::Informational => "informational",
334            Self::Minor => "minor",
335            Self::Major => "major",
336            Self::Blocking => "blocking",
337        }
338    }
339}
340
341/// Shared structure carried by an overlap witness.
342#[derive(Clone, Debug, Deserialize, PartialEq, Serialize)]
343#[serde(tag = "kind", content = "value", rename_all = "camelCase")]
344pub enum SharedStructure {
345    /// Feature set.
346    FeatureSet(Vec<Feature>),
347    /// Predicate set.
348    PredicateSet(Vec<Predicate>),
349    /// Normalized claim.
350    NormalizedClaim(NormalizedClaim),
351    /// Subgraph pattern.
352    Subgraph(SubgraphPattern),
353    /// Subcomplex pattern.
354    Subcomplex(SubcomplexPattern),
355    /// Invariant references.
356    ConstraintSet(Vec<Id>),
357    /// Evidence references.
358    EvidenceSet(Vec<Id>),
359    /// Boundary pattern.
360    Boundary(BoundaryPattern),
361    /// Projection trace.
362    ProjectionTrace(ProjectionTrace),
363    /// Causal pattern.
364    CausalPattern(CausalPattern),
365    /// Context restriction.
366    ContextRestriction(ContextRestriction),
367}
368
369/// Structure that differs across participants.
370#[derive(Clone, Debug, Deserialize, PartialEq, Serialize)]
371#[serde(tag = "kind", content = "value", rename_all = "camelCase")]
372pub enum DifferingStructure {
373    /// Additional detail by participant.
374    AdditionalDetail(BTreeMap<String, String>),
375    /// Missing detail by participant.
376    MissingDetail(BTreeMap<String, String>),
377    /// Type mismatch by participant.
378    TypeMismatch(BTreeMap<String, String>),
379    /// Predicate mismatch by participant.
380    PredicateMismatch(BTreeMap<String, String>),
381    /// Modality mismatch by participant.
382    ModalityMismatch(BTreeMap<String, String>),
383    /// Context mismatch by participant.
384    ContextMismatch(BTreeMap<String, String>),
385    /// Evidence mismatch by participant.
386    EvidenceMismatch(Vec<Id>),
387    /// Confidence mismatch by participant.
388    ConfidenceMismatch(BTreeMap<String, Confidence>),
389    /// Temporal mismatch by participant.
390    TemporalMismatch(BTreeMap<String, String>),
391    /// Invariant mismatch by participant.
392    InvariantMismatch(Vec<InvariantCheckResult>),
393    /// Contradiction details.
394    Contradiction(BTreeMap<String, String>),
395    /// Projection loss details.
396    ProjectionLoss(ProjectionLoss),
397}
398
399/// Named feature used by overlap extraction.
400#[derive(Clone, Debug, Deserialize, Eq, Hash, Ord, PartialEq, PartialOrd, Serialize)]
401#[serde(rename_all = "camelCase", deny_unknown_fields)]
402pub struct Feature {
403    /// Feature key.
404    pub key: String,
405    /// Feature value.
406    pub value: String,
407}
408
409/// Predicate triple or relation fragment used by overlap extraction.
410#[derive(Clone, Debug, Deserialize, Eq, Hash, Ord, PartialEq, PartialOrd, Serialize)]
411#[serde(rename_all = "camelCase", deny_unknown_fields)]
412pub struct Predicate {
413    /// Subject identifier or normalized label.
414    pub subject: String,
415    /// Relation or predicate name.
416    pub relation: String,
417    /// Object identifier or normalized label.
418    pub object: String,
419}
420
421/// Normalized claim fields used to make semantic overlap reviewable.
422#[derive(Clone, Debug, Deserialize, Eq, Hash, Ord, PartialEq, PartialOrd, Serialize)]
423#[serde(rename_all = "camelCase", deny_unknown_fields)]
424pub struct NormalizedClaim {
425    /// Subject identifier or normalized label.
426    pub subject: String,
427    /// Relation or predicate name.
428    pub relation: String,
429    /// Object identifier or normalized label.
430    pub object: String,
431    /// Optional modality such as observed, inferred, required, or forbidden.
432    #[serde(skip_serializing_if = "Option::is_none")]
433    pub modality: Option<String>,
434    /// Optional normalized time or validity interval.
435    #[serde(skip_serializing_if = "Option::is_none")]
436    pub temporal_scope: Option<String>,
437}
438
439/// Minimal subgraph pattern for exact structural overlap.
440#[derive(Clone, Debug, Default, Deserialize, Eq, Hash, Ord, PartialEq, PartialOrd, Serialize)]
441#[serde(rename_all = "camelCase", deny_unknown_fields)]
442pub struct SubgraphPattern {
443    /// Node identifiers or normalized labels.
444    #[serde(default, skip_serializing_if = "Vec::is_empty")]
445    pub node_ids: Vec<Id>,
446    /// Edge identifiers or normalized labels.
447    #[serde(default, skip_serializing_if = "Vec::is_empty")]
448    pub edge_ids: Vec<Id>,
449}
450
451/// Minimal subcomplex pattern for exact structural overlap.
452#[derive(Clone, Debug, Default, Deserialize, Eq, Hash, Ord, PartialEq, PartialOrd, Serialize)]
453#[serde(rename_all = "camelCase", deny_unknown_fields)]
454pub struct SubcomplexPattern {
455    /// Cell identifiers in the shared subcomplex.
456    #[serde(default, skip_serializing_if = "Vec::is_empty")]
457    pub cell_ids: Vec<Id>,
458    /// Incidence identifiers in the shared subcomplex.
459    #[serde(default, skip_serializing_if = "Vec::is_empty")]
460    pub incidence_ids: Vec<Id>,
461}
462
463/// Shared boundary pattern.
464#[derive(Clone, Debug, Default, Deserialize, Eq, Hash, Ord, PartialEq, PartialOrd, Serialize)]
465#[serde(rename_all = "camelCase", deny_unknown_fields)]
466pub struct BoundaryPattern {
467    /// Boundary cell identifiers.
468    #[serde(default, skip_serializing_if = "Vec::is_empty")]
469    pub boundary_cell_ids: Vec<Id>,
470}
471
472/// Trace from projected view back to source structures.
473#[derive(Clone, Debug, Default, Deserialize, Eq, Hash, Ord, PartialEq, PartialOrd, Serialize)]
474#[serde(rename_all = "camelCase", deny_unknown_fields)]
475pub struct ProjectionTrace {
476    /// Source structure identifiers.
477    #[serde(default, skip_serializing_if = "Vec::is_empty")]
478    pub source_ids: Vec<Id>,
479    /// Projection identifiers.
480    #[serde(default, skip_serializing_if = "Vec::is_empty")]
481    pub projection_ids: Vec<Id>,
482}
483
484/// Causal pattern shared across participants.
485#[derive(Clone, Debug, Default, Deserialize, Eq, Hash, Ord, PartialEq, PartialOrd, Serialize)]
486#[serde(rename_all = "camelCase", deny_unknown_fields)]
487pub struct CausalPattern {
488    /// Cause identifiers.
489    #[serde(default, skip_serializing_if = "Vec::is_empty")]
490    pub cause_ids: Vec<Id>,
491    /// Effect identifiers.
492    #[serde(default, skip_serializing_if = "Vec::is_empty")]
493    pub effect_ids: Vec<Id>,
494    /// Path identifiers.
495    #[serde(default, skip_serializing_if = "Vec::is_empty")]
496    pub path_ids: Vec<Id>,
497}
498
499/// Context restriction shared by participants.
500#[derive(Clone, Debug, Default, Deserialize, Eq, Hash, Ord, PartialEq, PartialOrd, Serialize)]
501#[serde(rename_all = "camelCase", deny_unknown_fields)]
502pub struct ContextRestriction {
503    /// Source context identifiers.
504    #[serde(default, skip_serializing_if = "Vec::is_empty")]
505    pub source_context_ids: Vec<Id>,
506    /// Restricted context identifier.
507    #[serde(skip_serializing_if = "Option::is_none")]
508    pub target_context_id: Option<Id>,
509    /// Elements retained by the restriction.
510    #[serde(default, skip_serializing_if = "Vec::is_empty")]
511    pub retained_element_ids: Vec<Id>,
512}
513
514/// Mapping from a participant into a witness path.
515#[derive(Clone, Debug, Deserialize, Eq, Hash, Ord, PartialEq, PartialOrd, Serialize)]
516#[serde(rename_all = "camelCase", deny_unknown_fields)]
517pub struct ParticipantMapping {
518    /// Participant identifier being mapped.
519    pub participant: Id,
520    /// Structured path inside the participant payload.
521    pub path: String,
522}
523
524/// Scope in which a witness is valid.
525#[derive(Clone, Debug, Default, Deserialize, Eq, Hash, Ord, PartialEq, PartialOrd, Serialize)]
526#[serde(rename_all = "camelCase", deny_unknown_fields)]
527pub struct Scope {
528    /// Structure identifiers covered by the scope.
529    #[serde(default, skip_serializing_if = "Vec::is_empty")]
530    pub structure_ids: Vec<Id>,
531    /// Optional textual boundary for downstream-specific scopes.
532    #[serde(skip_serializing_if = "Option::is_none")]
533    pub boundary: Option<String>,
534}
535
536/// Reviewable evidence for what is shared inside a correspondence.
537#[derive(Clone, Debug, Deserialize, PartialEq, Serialize)]
538#[serde(rename_all = "camelCase", deny_unknown_fields)]
539pub struct OverlapWitness {
540    /// Witness identifier.
541    pub id: Id,
542    /// Witness kind.
543    pub witness_kind: OverlapWitnessKind,
544    /// Concrete shared structure.
545    pub shared_structure: SharedStructure,
546    /// Participant-to-shared-structure mappings.
547    #[serde(default, skip_serializing_if = "Vec::is_empty")]
548    pub participant_mappings: Vec<ParticipantMapping>,
549    /// Scope in which the witness applies.
550    #[serde(default, skip_serializing_if = "Scope::is_empty")]
551    pub scope: Scope,
552    /// Context in which the witness is valid.
553    pub context: Id,
554    /// Evidence identifiers supporting the witness.
555    #[serde(default, skip_serializing_if = "Vec::is_empty")]
556    pub evidence: Vec<Id>,
557    /// Confidence in the witness.
558    pub confidence: Confidence,
559    /// Review status of this witness.
560    pub status: ReviewStatus,
561}
562
563impl OverlapWitness {
564    /// Returns true when this witness can support an accepted semantic overlap.
565    #[must_use]
566    pub fn supports_accepted_semantic_overlap(&self) -> bool {
567        self.witness_kind.supports_accepted_semantic_overlap()
568    }
569}
570
571/// Reviewable evidence for what differs inside a correspondence.
572#[derive(Clone, Debug, Deserialize, PartialEq, Serialize)]
573#[serde(rename_all = "camelCase", deny_unknown_fields)]
574pub struct DifferenceWitness {
575    /// Witness identifier.
576    pub id: Id,
577    /// Difference kind.
578    pub difference_kind: DifferenceKind,
579    /// Concrete differing structure.
580    pub differing_structure: DifferingStructure,
581    /// Participant-to-difference mappings.
582    #[serde(default, skip_serializing_if = "Vec::is_empty")]
583    pub participant_mappings: Vec<ParticipantMapping>,
584    /// Difference severity.
585    pub severity: DifferenceSeverity,
586    /// Context in which the difference is valid.
587    pub context: Id,
588    /// Evidence identifiers supporting the difference.
589    #[serde(default, skip_serializing_if = "Vec::is_empty")]
590    pub evidence: Vec<Id>,
591    /// Confidence in the difference.
592    pub confidence: Confidence,
593    /// Review status of this witness.
594    pub status: ReviewStatus,
595}
596
597/// Result of checking an invariant during gluing.
598#[derive(Clone, Debug, Deserialize, Eq, Hash, Ord, PartialEq, PartialOrd, Serialize)]
599#[serde(rename_all = "camelCase", deny_unknown_fields)]
600pub struct InvariantCheckResult {
601    /// Invariant identifier.
602    pub invariant: Id,
603    /// Stable result label such as passed, failed, skipped, or unknown.
604    pub result: String,
605    /// Optional diagnostic detail.
606    #[serde(skip_serializing_if = "Option::is_none")]
607    pub detail: Option<String>,
608}
609
610/// Report of structures preserved by gluing or projection.
611#[derive(Clone, Debug, Default, Deserialize, Eq, Hash, Ord, PartialEq, PartialOrd, Serialize)]
612#[serde(rename_all = "camelCase", deny_unknown_fields)]
613pub struct PreservationReport {
614    /// Preserved invariant identifiers.
615    #[serde(default, skip_serializing_if = "Vec::is_empty")]
616    pub preserved_invariants: Vec<Id>,
617    /// Preserved structure identifiers.
618    #[serde(default, skip_serializing_if = "Vec::is_empty")]
619    pub preserved_structures: Vec<Id>,
620    /// Optional summary.
621    #[serde(skip_serializing_if = "Option::is_none")]
622    pub summary: Option<String>,
623}
624
625/// Declared projection loss for correspondence views.
626#[derive(Clone, Debug, Default, Deserialize, Eq, Hash, Ord, PartialEq, PartialOrd, Serialize)]
627#[serde(rename_all = "camelCase", deny_unknown_fields)]
628pub struct ProjectionLoss {
629    /// Overlap witnesses omitted from a projection.
630    #[serde(default, skip_serializing_if = "Vec::is_empty")]
631    pub omitted_overlap_witnesses: Vec<Id>,
632    /// Difference witnesses omitted from a projection.
633    #[serde(default, skip_serializing_if = "Vec::is_empty")]
634    pub omitted_difference_witnesses: Vec<Id>,
635    /// Evidence omitted from a projection.
636    #[serde(default, skip_serializing_if = "Vec::is_empty")]
637    pub omitted_evidence: Vec<Id>,
638    /// Contexts omitted from a projection.
639    #[serde(default, skip_serializing_if = "Vec::is_empty")]
640    pub omitted_contexts: Vec<Id>,
641    /// Review-status collapses made by a projection.
642    #[serde(default, skip_serializing_if = "Vec::is_empty")]
643    pub collapsed_statuses: Vec<ReviewStatusCollapse>,
644}
645
646/// Explicit record that a projection collapsed one review status into another.
647#[derive(Clone, Debug, Deserialize, Eq, Hash, Ord, PartialEq, PartialOrd, Serialize)]
648#[serde(rename_all = "camelCase", deny_unknown_fields)]
649pub struct ReviewStatusCollapse {
650    /// Identifier whose status was collapsed.
651    pub source_id: Id,
652    /// Original review status.
653    pub from: ReviewStatus,
654    /// Rendered review status.
655    pub to: ReviewStatus,
656}
657
658/// Stable validation finding code for correspondence invariants.
659#[derive(Clone, Copy, Debug, Deserialize, Eq, Hash, Ord, PartialEq, PartialOrd, Serialize)]
660#[serde(rename_all = "snake_case")]
661pub enum CorrespondenceValidationCode {
662    /// A correspondence had fewer than two participants.
663    MissingParticipants,
664    /// An accepted correspondence lacked supporting evidence.
665    AcceptedMissingEvidence,
666    /// A conflicting correspondence lacked shared structure.
667    ConflictMissingSharedStructure,
668    /// Accepted semantic overlap lacked an explicit semantic witness.
669    SemanticOverlapMissingExplicitWitness,
670    /// A gluing success lacked a preservation report.
671    GluingSuccessMissingPreservationReport,
672    /// A blocking difference was silently merged.
673    BlockingDifferenceSilentMerge,
674}
675
676/// One validation finding for a correspondence cell.
677#[derive(Clone, Debug, Deserialize, Eq, Hash, Ord, PartialEq, PartialOrd, Serialize)]
678#[serde(rename_all = "camelCase", deny_unknown_fields)]
679pub struct CorrespondenceValidationFinding {
680    /// Stable finding code.
681    pub code: CorrespondenceValidationCode,
682    /// Field where the invariant failed.
683    pub field: String,
684    /// Human-readable reason.
685    pub reason: String,
686}
687
688impl CorrespondenceValidationFinding {
689    fn new(
690        code: CorrespondenceValidationCode,
691        field: impl Into<String>,
692        reason: impl Into<String>,
693    ) -> Self {
694        Self {
695            code,
696            field: field.into(),
697            reason: reason.into(),
698        }
699    }
700}
701
702/// Multi-finding validation report for correspondence cells.
703#[derive(Clone, Debug, Deserialize, Eq, PartialEq, Serialize)]
704#[serde(rename_all = "camelCase", deny_unknown_fields)]
705pub struct CorrespondenceValidationReport {
706    /// Correspondence being validated.
707    pub correspondence_id: Id,
708    /// Validation findings. Empty means valid for Phase 1 invariants.
709    #[serde(default, skip_serializing_if = "Vec::is_empty")]
710    pub findings: Vec<CorrespondenceValidationFinding>,
711}
712
713impl CorrespondenceValidationReport {
714    /// Returns true when there are no findings.
715    #[must_use]
716    pub fn is_valid(&self) -> bool {
717        self.findings.is_empty()
718    }
719}
720
721/// Result of trying to glue participants over overlap witnesses.
722#[derive(Clone, Debug, Deserialize, PartialEq, Serialize)]
723#[serde(tag = "kind", rename_all = "snake_case")]
724pub enum GluingResult {
725    /// Participants were safely glued.
726    Success {
727        /// Merged complex identifier when a concrete structure was materialized.
728        #[serde(rename = "mergedComplex", skip_serializing_if = "Option::is_none")]
729        merged_complex: Option<Id>,
730        /// Preservation report for the merge.
731        #[serde(rename = "preservationReport")]
732        preservation_report: PreservationReport,
733    },
734    /// Gluing is plausible but requires review.
735    Candidate {
736        /// Completion candidate identifier.
737        #[serde(rename = "completionCandidate")]
738        completion_candidate: Id,
739        /// Required review before acceptance.
740        #[serde(rename = "requiredReview")]
741        required_review: ReviewRequirement,
742    },
743    /// Gluing failed with a structured obstruction.
744    Failure {
745        /// Obstruction identifier.
746        obstruction: Id,
747    },
748}
749
750impl GluingResult {
751    /// Returns the stable serde discriminant string for this result variant.
752    #[must_use]
753    pub fn kind(&self) -> &'static str {
754        match self {
755            Self::Success { .. } => "success",
756            Self::Candidate { .. } => "candidate",
757            Self::Failure { .. } => "failure",
758        }
759    }
760}
761
762/// Full gluing attempt record.
763#[derive(Clone, Debug, Deserialize, PartialEq, Serialize)]
764#[serde(rename_all = "camelCase", deny_unknown_fields)]
765pub struct GluingAttempt {
766    /// Gluing attempt identifier.
767    pub id: Id,
768    /// Participants involved in the attempt.
769    #[serde(default, skip_serializing_if = "Vec::is_empty")]
770    pub participants: Vec<ParticipantRef>,
771    /// Overlap witness identifiers used by the attempt.
772    #[serde(default, skip_serializing_if = "Vec::is_empty")]
773    pub overlap_witnesses: Vec<Id>,
774    /// Difference witness identifiers used by the attempt.
775    #[serde(default, skip_serializing_if = "Vec::is_empty")]
776    pub difference_witnesses: Vec<Id>,
777    /// Context in which gluing was checked.
778    pub context: Id,
779    /// Invariant checks run during gluing.
780    #[serde(default, skip_serializing_if = "Vec::is_empty")]
781    pub invariant_checks: Vec<InvariantCheckResult>,
782    /// Structures preserved by the attempt.
783    #[serde(default, skip_serializing_if = "PreservationReport::is_empty")]
784    pub preservation_report: PreservationReport,
785    /// Attempt result.
786    pub result: GluingResult,
787    /// Evidence supporting the attempt.
788    #[serde(default, skip_serializing_if = "Vec::is_empty")]
789    pub evidence: Vec<Id>,
790    /// Confidence in the attempt.
791    pub confidence: Confidence,
792    /// Review status of the attempt.
793    pub status: ReviewStatus,
794    /// Explicit review override that permits success despite blocking differences.
795    #[serde(skip_serializing_if = "Option::is_none")]
796    pub override_review: Option<ReviewRequirement>,
797}
798
799/// Higher-order cell describing a correspondence between structures.
800#[derive(Clone, Debug, Deserialize, PartialEq, Serialize)]
801#[serde(rename_all = "camelCase", deny_unknown_fields)]
802pub struct CorrespondenceCell {
803    /// Correspondence identifier.
804    pub id: Id,
805    /// Role-labeled participants.
806    pub participants: Vec<CorrespondenceParticipant>,
807    /// Correspondence kind.
808    pub correspondence_kind: CorrespondenceKind,
809    /// Correspondence polarity.
810    pub polarity: CorrespondencePolarity,
811    /// Reviewable overlap witnesses.
812    #[serde(default, skip_serializing_if = "Vec::is_empty")]
813    pub overlap_witnesses: Vec<OverlapWitness>,
814    /// Reviewable difference witnesses.
815    #[serde(default, skip_serializing_if = "Vec::is_empty")]
816    pub difference_witnesses: Vec<DifferenceWitness>,
817    /// Context in which the correspondence is valid.
818    pub context: Id,
819    /// Evidence identifiers supporting the correspondence.
820    #[serde(default, skip_serializing_if = "Vec::is_empty")]
821    pub evidence: Vec<Id>,
822    /// Provenance reference.
823    pub provenance: Id,
824    /// Confidence in the correspondence.
825    pub confidence: Confidence,
826    /// Review status of the correspondence.
827    pub review_status: ReviewStatus,
828    /// Optional gluing attempt.
829    #[serde(skip_serializing_if = "Option::is_none")]
830    pub gluing: Option<GluingAttempt>,
831}
832
833impl CorrespondenceCell {
834    /// Returns a multi-finding validation report for Phase 1 correspondence invariants.
835    #[must_use]
836    pub fn validate_report(&self) -> CorrespondenceValidationReport {
837        let mut findings = Vec::new();
838
839        if self.participants.len() < 2 {
840            findings.push(CorrespondenceValidationFinding::new(
841                CorrespondenceValidationCode::MissingParticipants,
842                "participants",
843                "correspondence requires at least two participants",
844            ));
845        }
846
847        if self.review_status.is_accepted() && self.evidence.is_empty() {
848            findings.push(CorrespondenceValidationFinding::new(
849                CorrespondenceValidationCode::AcceptedMissingEvidence,
850                "evidence",
851                "accepted correspondence requires supporting evidence",
852            ));
853        }
854
855        if matches!(self.polarity, CorrespondencePolarity::Conflicting)
856            && self.overlap_witnesses.is_empty()
857        {
858            findings.push(CorrespondenceValidationFinding::new(
859                CorrespondenceValidationCode::ConflictMissingSharedStructure,
860                "overlap_witnesses",
861                "conflicting correspondence requires explicit shared structure",
862            ));
863        }
864
865        if matches!(
866            self.correspondence_kind,
867            CorrespondenceKind::SemanticOverlap
868        ) && self.review_status.is_accepted()
869            && !self
870                .overlap_witnesses
871                .iter()
872                .any(OverlapWitness::supports_accepted_semantic_overlap)
873        {
874            findings.push(CorrespondenceValidationFinding::new(
875                CorrespondenceValidationCode::SemanticOverlapMissingExplicitWitness,
876                "overlap_witnesses",
877                "accepted semantic overlap requires normalized claim, predicate set, or feature set witness",
878            ));
879        }
880
881        if let Some(gluing) = &self.gluing {
882            findings.extend(gluing.validation_findings(&self.difference_witnesses));
883        }
884
885        CorrespondenceValidationReport {
886            correspondence_id: self.id.clone(),
887            findings,
888        }
889    }
890
891    /// Validates Phase 1 correspondence invariants.
892    pub fn validate(&self) -> Result<()> {
893        let report = self.validate_report();
894        if let Some(finding) = report.findings.first() {
895            return Err(malformed_field(&finding.field, finding.reason.clone()));
896        }
897
898        Ok(())
899    }
900}
901
902impl GluingAttempt {
903    fn validation_findings(
904        &self,
905        differences: &[DifferenceWitness],
906    ) -> Vec<CorrespondenceValidationFinding> {
907        let mut findings = Vec::new();
908
909        if let GluingResult::Success {
910            preservation_report,
911            ..
912        } = &self.result
913        {
914            if preservation_report.is_empty() && self.preservation_report.is_empty() {
915                findings.push(CorrespondenceValidationFinding::new(
916                    CorrespondenceValidationCode::GluingSuccessMissingPreservationReport,
917                    "preservation_report",
918                    "gluing success requires a preservation report",
919                ));
920            }
921
922            if self.override_review.is_none()
923                && differences
924                    .iter()
925                    .any(|difference| matches!(difference.severity, DifferenceSeverity::Blocking))
926            {
927                findings.push(CorrespondenceValidationFinding::new(
928                    CorrespondenceValidationCode::BlockingDifferenceSilentMerge,
929                    "result",
930                    "blocking difference prevents gluing success without explicit override review",
931                ));
932            }
933        }
934
935        findings
936    }
937
938    /// Validates gluing invariants that do not require external graph lookup.
939    pub fn validate_with_differences(&self, differences: &[DifferenceWitness]) -> Result<()> {
940        let findings = self.validation_findings(differences);
941        if let Some(finding) = findings.first() {
942            return Err(malformed_field(&finding.field, finding.reason.clone()));
943        }
944
945        Ok(())
946    }
947}
948
949impl Scope {
950    fn is_empty(&self) -> bool {
951        self.structure_ids.is_empty() && self.boundary.is_none()
952    }
953}
954
955impl PreservationReport {
956    fn is_empty(&self) -> bool {
957        self.preserved_invariants.is_empty()
958            && self.preserved_structures.is_empty()
959            && self.summary.is_none()
960    }
961}
962
963fn required_text(field: impl Into<String>, value: impl Into<String>) -> Result<String> {
964    let raw = value.into();
965    let normalized = raw.trim().to_owned();
966
967    if normalized.is_empty() {
968        return Err(malformed_field(
969            field,
970            "value must not be empty after trimming",
971        ));
972    }
973
974    Ok(normalized)
975}
976
977fn malformed_field(field: impl Into<String>, reason: impl Into<String>) -> CoreError {
978    CoreError::MalformedField {
979        field: field.into(),
980        reason: reason.into(),
981    }
982}