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#[derive(Clone, Debug, Eq, Hash, Ord, PartialEq, PartialOrd, Serialize)]
9#[serde(tag = "kind", content = "id", rename_all = "camelCase")]
10pub enum ParticipantRef {
11 Cell(Id),
13 Complex(Id),
15 Context(Id),
17 Projection(Id),
19 Claim(Id),
21 Evidence(Id),
23 Invariant(Id),
25 Obstruction(Id),
27 CompletionCandidate(Id),
29}
30
31impl ParticipantRef {
32 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 #[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#[derive(Clone, Debug, Deserialize, Eq, Hash, Ord, PartialEq, PartialOrd, Serialize)]
160#[serde(rename_all = "camelCase", deny_unknown_fields)]
161pub struct CorrespondenceParticipant {
162 pub role: String,
164 #[serde(rename = "ref")]
166 pub participant: ParticipantRef,
167}
168
169impl CorrespondenceParticipant {
170 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#[derive(Clone, Copy, Debug, Deserialize, Eq, Hash, Ord, PartialEq, PartialOrd, Serialize)]
181pub enum CorrespondenceKind {
182 ExactIdentity,
184 SurfaceOverlap,
186 SemanticOverlap,
188 StructuralOverlap,
190 ConstraintOverlap,
192 EvidenceOverlap,
194 ContextualOverlap,
196 CausalOverlap,
198 ProjectionOverlap,
200 Refinement,
202 Abstraction,
204 Conflict,
206 Synergy,
208 Obstructed,
210}
211
212#[derive(Clone, Copy, Debug, Deserialize, Eq, Hash, Ord, PartialEq, PartialOrd, Serialize)]
214pub enum CorrespondencePolarity {
215 Agreeing,
217 Conflicting,
219 Refining,
221 Projecting,
223 Ambiguous,
225 Unknown,
227}
228
229#[derive(Clone, Copy, Debug, Deserialize, Eq, Hash, Ord, PartialEq, PartialOrd, Serialize)]
231pub enum OverlapWitnessKind {
232 FeatureSet,
234 PredicateSet,
236 NormalizedClaim,
238 Subgraph,
240 Subcomplex,
242 ConstraintSet,
244 EvidenceSet,
246 Boundary,
248 ProjectionTrace,
250 CausalPattern,
252 ContextRestriction,
254}
255
256impl OverlapWitnessKind {
257 #[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 #[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#[derive(Clone, Copy, Debug, Deserialize, Eq, Hash, Ord, PartialEq, PartialOrd, Serialize)]
287pub enum DifferenceKind {
288 AdditionalDetail,
290 MissingDetail,
292 TypeMismatch,
294 PredicateMismatch,
296 ModalityMismatch,
298 ContextMismatch,
300 EvidenceMismatch,
302 ConfidenceMismatch,
304 TemporalMismatch,
306 InvariantMismatch,
308 Contradiction,
310 ProjectionLoss,
312}
313
314#[derive(Clone, Copy, Debug, Deserialize, Eq, Hash, Ord, PartialEq, PartialOrd, Serialize)]
316#[serde(rename_all = "snake_case")]
317pub enum DifferenceSeverity {
318 Informational,
320 Minor,
322 Major,
324 Blocking,
326}
327
328impl DifferenceSeverity {
329 #[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#[derive(Clone, Debug, Deserialize, PartialEq, Serialize)]
343#[serde(tag = "kind", content = "value", rename_all = "camelCase")]
344pub enum SharedStructure {
345 FeatureSet(Vec<Feature>),
347 PredicateSet(Vec<Predicate>),
349 NormalizedClaim(NormalizedClaim),
351 Subgraph(SubgraphPattern),
353 Subcomplex(SubcomplexPattern),
355 ConstraintSet(Vec<Id>),
357 EvidenceSet(Vec<Id>),
359 Boundary(BoundaryPattern),
361 ProjectionTrace(ProjectionTrace),
363 CausalPattern(CausalPattern),
365 ContextRestriction(ContextRestriction),
367}
368
369#[derive(Clone, Debug, Deserialize, PartialEq, Serialize)]
371#[serde(tag = "kind", content = "value", rename_all = "camelCase")]
372pub enum DifferingStructure {
373 AdditionalDetail(BTreeMap<String, String>),
375 MissingDetail(BTreeMap<String, String>),
377 TypeMismatch(BTreeMap<String, String>),
379 PredicateMismatch(BTreeMap<String, String>),
381 ModalityMismatch(BTreeMap<String, String>),
383 ContextMismatch(BTreeMap<String, String>),
385 EvidenceMismatch(Vec<Id>),
387 ConfidenceMismatch(BTreeMap<String, Confidence>),
389 TemporalMismatch(BTreeMap<String, String>),
391 InvariantMismatch(Vec<InvariantCheckResult>),
393 Contradiction(BTreeMap<String, String>),
395 ProjectionLoss(ProjectionLoss),
397}
398
399#[derive(Clone, Debug, Deserialize, Eq, Hash, Ord, PartialEq, PartialOrd, Serialize)]
401#[serde(rename_all = "camelCase", deny_unknown_fields)]
402pub struct Feature {
403 pub key: String,
405 pub value: String,
407}
408
409#[derive(Clone, Debug, Deserialize, Eq, Hash, Ord, PartialEq, PartialOrd, Serialize)]
411#[serde(rename_all = "camelCase", deny_unknown_fields)]
412pub struct Predicate {
413 pub subject: String,
415 pub relation: String,
417 pub object: String,
419}
420
421#[derive(Clone, Debug, Deserialize, Eq, Hash, Ord, PartialEq, PartialOrd, Serialize)]
423#[serde(rename_all = "camelCase", deny_unknown_fields)]
424pub struct NormalizedClaim {
425 pub subject: String,
427 pub relation: String,
429 pub object: String,
431 #[serde(skip_serializing_if = "Option::is_none")]
433 pub modality: Option<String>,
434 #[serde(skip_serializing_if = "Option::is_none")]
436 pub temporal_scope: Option<String>,
437}
438
439#[derive(Clone, Debug, Default, Deserialize, Eq, Hash, Ord, PartialEq, PartialOrd, Serialize)]
441#[serde(rename_all = "camelCase", deny_unknown_fields)]
442pub struct SubgraphPattern {
443 #[serde(default, skip_serializing_if = "Vec::is_empty")]
445 pub node_ids: Vec<Id>,
446 #[serde(default, skip_serializing_if = "Vec::is_empty")]
448 pub edge_ids: Vec<Id>,
449}
450
451#[derive(Clone, Debug, Default, Deserialize, Eq, Hash, Ord, PartialEq, PartialOrd, Serialize)]
453#[serde(rename_all = "camelCase", deny_unknown_fields)]
454pub struct SubcomplexPattern {
455 #[serde(default, skip_serializing_if = "Vec::is_empty")]
457 pub cell_ids: Vec<Id>,
458 #[serde(default, skip_serializing_if = "Vec::is_empty")]
460 pub incidence_ids: Vec<Id>,
461}
462
463#[derive(Clone, Debug, Default, Deserialize, Eq, Hash, Ord, PartialEq, PartialOrd, Serialize)]
465#[serde(rename_all = "camelCase", deny_unknown_fields)]
466pub struct BoundaryPattern {
467 #[serde(default, skip_serializing_if = "Vec::is_empty")]
469 pub boundary_cell_ids: Vec<Id>,
470}
471
472#[derive(Clone, Debug, Default, Deserialize, Eq, Hash, Ord, PartialEq, PartialOrd, Serialize)]
474#[serde(rename_all = "camelCase", deny_unknown_fields)]
475pub struct ProjectionTrace {
476 #[serde(default, skip_serializing_if = "Vec::is_empty")]
478 pub source_ids: Vec<Id>,
479 #[serde(default, skip_serializing_if = "Vec::is_empty")]
481 pub projection_ids: Vec<Id>,
482}
483
484#[derive(Clone, Debug, Default, Deserialize, Eq, Hash, Ord, PartialEq, PartialOrd, Serialize)]
486#[serde(rename_all = "camelCase", deny_unknown_fields)]
487pub struct CausalPattern {
488 #[serde(default, skip_serializing_if = "Vec::is_empty")]
490 pub cause_ids: Vec<Id>,
491 #[serde(default, skip_serializing_if = "Vec::is_empty")]
493 pub effect_ids: Vec<Id>,
494 #[serde(default, skip_serializing_if = "Vec::is_empty")]
496 pub path_ids: Vec<Id>,
497}
498
499#[derive(Clone, Debug, Default, Deserialize, Eq, Hash, Ord, PartialEq, PartialOrd, Serialize)]
501#[serde(rename_all = "camelCase", deny_unknown_fields)]
502pub struct ContextRestriction {
503 #[serde(default, skip_serializing_if = "Vec::is_empty")]
505 pub source_context_ids: Vec<Id>,
506 #[serde(skip_serializing_if = "Option::is_none")]
508 pub target_context_id: Option<Id>,
509 #[serde(default, skip_serializing_if = "Vec::is_empty")]
511 pub retained_element_ids: Vec<Id>,
512}
513
514#[derive(Clone, Debug, Deserialize, Eq, Hash, Ord, PartialEq, PartialOrd, Serialize)]
516#[serde(rename_all = "camelCase", deny_unknown_fields)]
517pub struct ParticipantMapping {
518 pub participant: Id,
520 pub path: String,
522}
523
524#[derive(Clone, Debug, Default, Deserialize, Eq, Hash, Ord, PartialEq, PartialOrd, Serialize)]
526#[serde(rename_all = "camelCase", deny_unknown_fields)]
527pub struct Scope {
528 #[serde(default, skip_serializing_if = "Vec::is_empty")]
530 pub structure_ids: Vec<Id>,
531 #[serde(skip_serializing_if = "Option::is_none")]
533 pub boundary: Option<String>,
534}
535
536#[derive(Clone, Debug, Deserialize, PartialEq, Serialize)]
538#[serde(rename_all = "camelCase", deny_unknown_fields)]
539pub struct OverlapWitness {
540 pub id: Id,
542 pub witness_kind: OverlapWitnessKind,
544 pub shared_structure: SharedStructure,
546 #[serde(default, skip_serializing_if = "Vec::is_empty")]
548 pub participant_mappings: Vec<ParticipantMapping>,
549 #[serde(default, skip_serializing_if = "Scope::is_empty")]
551 pub scope: Scope,
552 pub context: Id,
554 #[serde(default, skip_serializing_if = "Vec::is_empty")]
556 pub evidence: Vec<Id>,
557 pub confidence: Confidence,
559 pub status: ReviewStatus,
561}
562
563impl OverlapWitness {
564 #[must_use]
566 pub fn supports_accepted_semantic_overlap(&self) -> bool {
567 self.witness_kind.supports_accepted_semantic_overlap()
568 }
569}
570
571#[derive(Clone, Debug, Deserialize, PartialEq, Serialize)]
573#[serde(rename_all = "camelCase", deny_unknown_fields)]
574pub struct DifferenceWitness {
575 pub id: Id,
577 pub difference_kind: DifferenceKind,
579 pub differing_structure: DifferingStructure,
581 #[serde(default, skip_serializing_if = "Vec::is_empty")]
583 pub participant_mappings: Vec<ParticipantMapping>,
584 pub severity: DifferenceSeverity,
586 pub context: Id,
588 #[serde(default, skip_serializing_if = "Vec::is_empty")]
590 pub evidence: Vec<Id>,
591 pub confidence: Confidence,
593 pub status: ReviewStatus,
595}
596
597#[derive(Clone, Debug, Deserialize, Eq, Hash, Ord, PartialEq, PartialOrd, Serialize)]
599#[serde(rename_all = "camelCase", deny_unknown_fields)]
600pub struct InvariantCheckResult {
601 pub invariant: Id,
603 pub result: String,
605 #[serde(skip_serializing_if = "Option::is_none")]
607 pub detail: Option<String>,
608}
609
610#[derive(Clone, Debug, Default, Deserialize, Eq, Hash, Ord, PartialEq, PartialOrd, Serialize)]
612#[serde(rename_all = "camelCase", deny_unknown_fields)]
613pub struct PreservationReport {
614 #[serde(default, skip_serializing_if = "Vec::is_empty")]
616 pub preserved_invariants: Vec<Id>,
617 #[serde(default, skip_serializing_if = "Vec::is_empty")]
619 pub preserved_structures: Vec<Id>,
620 #[serde(skip_serializing_if = "Option::is_none")]
622 pub summary: Option<String>,
623}
624
625#[derive(Clone, Debug, Default, Deserialize, Eq, Hash, Ord, PartialEq, PartialOrd, Serialize)]
627#[serde(rename_all = "camelCase", deny_unknown_fields)]
628pub struct ProjectionLoss {
629 #[serde(default, skip_serializing_if = "Vec::is_empty")]
631 pub omitted_overlap_witnesses: Vec<Id>,
632 #[serde(default, skip_serializing_if = "Vec::is_empty")]
634 pub omitted_difference_witnesses: Vec<Id>,
635 #[serde(default, skip_serializing_if = "Vec::is_empty")]
637 pub omitted_evidence: Vec<Id>,
638 #[serde(default, skip_serializing_if = "Vec::is_empty")]
640 pub omitted_contexts: Vec<Id>,
641 #[serde(default, skip_serializing_if = "Vec::is_empty")]
643 pub collapsed_statuses: Vec<ReviewStatusCollapse>,
644}
645
646#[derive(Clone, Debug, Deserialize, Eq, Hash, Ord, PartialEq, PartialOrd, Serialize)]
648#[serde(rename_all = "camelCase", deny_unknown_fields)]
649pub struct ReviewStatusCollapse {
650 pub source_id: Id,
652 pub from: ReviewStatus,
654 pub to: ReviewStatus,
656}
657
658#[derive(Clone, Copy, Debug, Deserialize, Eq, Hash, Ord, PartialEq, PartialOrd, Serialize)]
660#[serde(rename_all = "snake_case")]
661pub enum CorrespondenceValidationCode {
662 MissingParticipants,
664 AcceptedMissingEvidence,
666 ConflictMissingSharedStructure,
668 SemanticOverlapMissingExplicitWitness,
670 GluingSuccessMissingPreservationReport,
672 BlockingDifferenceSilentMerge,
674}
675
676#[derive(Clone, Debug, Deserialize, Eq, Hash, Ord, PartialEq, PartialOrd, Serialize)]
678#[serde(rename_all = "camelCase", deny_unknown_fields)]
679pub struct CorrespondenceValidationFinding {
680 pub code: CorrespondenceValidationCode,
682 pub field: String,
684 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#[derive(Clone, Debug, Deserialize, Eq, PartialEq, Serialize)]
704#[serde(rename_all = "camelCase", deny_unknown_fields)]
705pub struct CorrespondenceValidationReport {
706 pub correspondence_id: Id,
708 #[serde(default, skip_serializing_if = "Vec::is_empty")]
710 pub findings: Vec<CorrespondenceValidationFinding>,
711}
712
713impl CorrespondenceValidationReport {
714 #[must_use]
716 pub fn is_valid(&self) -> bool {
717 self.findings.is_empty()
718 }
719}
720
721#[derive(Clone, Debug, Deserialize, PartialEq, Serialize)]
723#[serde(tag = "kind", rename_all = "snake_case")]
724pub enum GluingResult {
725 Success {
727 #[serde(rename = "mergedComplex", skip_serializing_if = "Option::is_none")]
729 merged_complex: Option<Id>,
730 #[serde(rename = "preservationReport")]
732 preservation_report: PreservationReport,
733 },
734 Candidate {
736 #[serde(rename = "completionCandidate")]
738 completion_candidate: Id,
739 #[serde(rename = "requiredReview")]
741 required_review: ReviewRequirement,
742 },
743 Failure {
745 obstruction: Id,
747 },
748}
749
750impl GluingResult {
751 #[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#[derive(Clone, Debug, Deserialize, PartialEq, Serialize)]
764#[serde(rename_all = "camelCase", deny_unknown_fields)]
765pub struct GluingAttempt {
766 pub id: Id,
768 #[serde(default, skip_serializing_if = "Vec::is_empty")]
770 pub participants: Vec<ParticipantRef>,
771 #[serde(default, skip_serializing_if = "Vec::is_empty")]
773 pub overlap_witnesses: Vec<Id>,
774 #[serde(default, skip_serializing_if = "Vec::is_empty")]
776 pub difference_witnesses: Vec<Id>,
777 pub context: Id,
779 #[serde(default, skip_serializing_if = "Vec::is_empty")]
781 pub invariant_checks: Vec<InvariantCheckResult>,
782 #[serde(default, skip_serializing_if = "PreservationReport::is_empty")]
784 pub preservation_report: PreservationReport,
785 pub result: GluingResult,
787 #[serde(default, skip_serializing_if = "Vec::is_empty")]
789 pub evidence: Vec<Id>,
790 pub confidence: Confidence,
792 pub status: ReviewStatus,
794 #[serde(skip_serializing_if = "Option::is_none")]
796 pub override_review: Option<ReviewRequirement>,
797}
798
799#[derive(Clone, Debug, Deserialize, PartialEq, Serialize)]
801#[serde(rename_all = "camelCase", deny_unknown_fields)]
802pub struct CorrespondenceCell {
803 pub id: Id,
805 pub participants: Vec<CorrespondenceParticipant>,
807 pub correspondence_kind: CorrespondenceKind,
809 pub polarity: CorrespondencePolarity,
811 #[serde(default, skip_serializing_if = "Vec::is_empty")]
813 pub overlap_witnesses: Vec<OverlapWitness>,
814 #[serde(default, skip_serializing_if = "Vec::is_empty")]
816 pub difference_witnesses: Vec<DifferenceWitness>,
817 pub context: Id,
819 #[serde(default, skip_serializing_if = "Vec::is_empty")]
821 pub evidence: Vec<Id>,
822 pub provenance: Id,
824 pub confidence: Confidence,
826 pub review_status: ReviewStatus,
828 #[serde(skip_serializing_if = "Option::is_none")]
830 pub gluing: Option<GluingAttempt>,
831}
832
833impl CorrespondenceCell {
834 #[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 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 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}