1use std::collections::{BTreeMap, BTreeSet};
4use std::error::Error;
5use std::fmt;
6
7use type_bridge_contract::fingerprint::SemanticProfileId;
8use type_bridge_contract::id::TypeKind;
9use type_bridge_contract::schema::{
10 AnnotationKindId, DeclaredIdentityFingerprint, DocumentId, FunctionReturnMode, InterfaceKind,
11 SchemaAnnotationValue, SchemaDiagnostics, SchemaFact, SemanticProfile,
12 SemanticSchemaFingerprint, TypeReference,
13};
14use type_bridge_contract::value::{CanonicalValue, Cardinality as V2Cardinality, ValueTypeTag};
15use type_bridge_core_lib::_parser as parser;
16use type_bridge_core_lib::_schema::{
17 Cardinality as V1Cardinality, FunctionType as V1Function, OwnedAttribute, PlayedRole, RoleSpec,
18 SchemaError, StructType as V1Struct, TypeSchema,
19};
20use type_bridge_schema::{ResolvedSchema, resolve};
21
22use crate::typeql_to_declared;
23
24const SHADOW_DOCUMENT: &str = "v1-shadow.typeql";
25
26#[derive(Clone, Copy, Debug, Eq, Ord, PartialEq, PartialOrd)]
28pub enum ShadowDimension {
29 TypeExistence,
31 TypeKind,
33 DirectParent,
35 TypeAbstract,
37 AttributeIndependent,
39 ValueType,
41 EffectiveOwns,
43 EffectiveRelates,
45 EffectivePlays,
47 DocumentationAndMetadata,
49 FunctionSignatures,
51 FunctionBodiesAndAnnotations,
53 StructFields,
55 SourceCommentsAndSpans,
57 OmittedVersusExplicitIdentity,
59 IndependentAnnotationIdentityAndRemoval,
61 SubAnnotations,
63 ExtensionsAndCapabilities,
65 ResolverGraphsAndOrigins,
67 CardinalityOutsideV1U32,
69}
70
71#[derive(Clone, Debug, Eq, PartialEq)]
73pub struct ShadowCoverage {
74 compared: BTreeSet<ShadowDimension>,
75 unimplemented: BTreeSet<ShadowDimension>,
76 not_representable: BTreeSet<ShadowDimension>,
77 blind_spots: BTreeSet<ShadowDimension>,
78}
79
80#[derive(Clone, Copy, Debug, Eq, PartialEq)]
82pub enum ShadowCoverageState {
83 Compared,
85 Unimplemented,
87 NotRepresentable,
89}
90
91impl ShadowCoverage {
92 #[must_use]
94 pub const fn compared(&self) -> &BTreeSet<ShadowDimension> {
95 &self.compared
96 }
97
98 #[must_use]
100 pub const fn covered(&self) -> &BTreeSet<ShadowDimension> {
101 self.compared()
102 }
103
104 #[must_use]
106 pub const fn unimplemented(&self) -> &BTreeSet<ShadowDimension> {
107 &self.unimplemented
108 }
109
110 #[must_use]
112 pub const fn not_representable(&self) -> &BTreeSet<ShadowDimension> {
113 &self.not_representable
114 }
115
116 #[must_use]
118 pub const fn blind_spots(&self) -> &BTreeSet<ShadowDimension> {
119 &self.blind_spots
120 }
121
122 #[must_use]
124 pub fn state(&self, dimension: ShadowDimension) -> ShadowCoverageState {
125 if self.compared.contains(&dimension) {
126 ShadowCoverageState::Compared
127 } else if self.unimplemented.contains(&dimension) {
128 ShadowCoverageState::Unimplemented
129 } else {
130 debug_assert!(self.not_representable.contains(&dimension));
131 ShadowCoverageState::NotRepresentable
132 }
133 }
134
135 #[must_use]
137 pub fn is_complete(&self) -> bool {
138 self.blind_spots.is_empty()
139 }
140}
141
142#[derive(Clone, Debug, Eq, PartialEq)]
144pub enum ShadowLaneOutcome {
145 Accepted(ShadowLaneSummary),
147 Rejected(ShadowLaneRejection),
149 NotRun(ShadowLaneNotRun),
151}
152
153impl ShadowLaneOutcome {
154 #[must_use]
156 pub const fn is_accepted(&self) -> bool {
157 matches!(self, Self::Accepted(_))
158 }
159}
160
161#[derive(Clone, Debug, Eq, PartialEq)]
163pub struct ShadowLaneSummary {
164 type_count: usize,
165}
166
167impl ShadowLaneSummary {
168 fn new(type_count: usize) -> Self {
169 Self { type_count }
170 }
171
172 #[must_use]
174 pub const fn type_count(&self) -> usize {
175 self.type_count
176 }
177}
178
179#[derive(Clone, Debug, Eq, PartialEq)]
181pub struct ShadowLaneRejection {
182 code: String,
183 message: String,
184}
185
186impl ShadowLaneRejection {
187 fn new(code: impl Into<String>, message: impl Into<String>) -> Self {
188 Self {
189 code: code.into(),
190 message: message.into(),
191 }
192 }
193
194 #[must_use]
196 pub fn code(&self) -> &str {
197 &self.code
198 }
199
200 #[must_use]
202 pub fn message(&self) -> &str {
203 &self.message
204 }
205}
206
207#[derive(Clone, Debug, Eq, PartialEq)]
209pub struct ShadowLaneNotRun {
210 code: &'static str,
211}
212
213impl ShadowLaneNotRun {
214 fn new(code: &'static str) -> Self {
215 Self { code }
216 }
217
218 #[must_use]
220 pub const fn code(&self) -> &'static str {
221 self.code
222 }
223}
224
225#[derive(Clone, Copy, Debug, Eq, PartialEq)]
227pub enum ShadowVerdict {
228 Matched,
230 Mismatched,
232}
233
234#[derive(Clone, Debug, Eq, Ord, PartialEq, PartialOrd)]
236pub struct ShadowFinding {
237 dimension: ShadowDimension,
238 type_label: String,
239 v1_value: Option<String>,
240 v2_value: Option<String>,
241}
242
243impl ShadowFinding {
244 fn new(
245 dimension: ShadowDimension,
246 type_label: impl Into<String>,
247 v1_value: Option<String>,
248 v2_value: Option<String>,
249 ) -> Self {
250 Self {
251 dimension,
252 type_label: type_label.into(),
253 v1_value,
254 v2_value,
255 }
256 }
257
258 #[must_use]
260 pub const fn dimension(&self) -> ShadowDimension {
261 self.dimension
262 }
263
264 #[must_use]
266 pub fn type_label(&self) -> &str {
267 &self.type_label
268 }
269
270 #[must_use]
272 pub fn v1_value(&self) -> Option<&str> {
273 self.v1_value.as_deref()
274 }
275
276 #[must_use]
278 pub fn v2_value(&self) -> Option<&str> {
279 self.v2_value.as_deref()
280 }
281}
282
283#[derive(Clone, Debug, Eq, PartialEq)]
285pub struct ShadowCompared {
286 verdict: ShadowVerdict,
287 coverage: ShadowCoverage,
288 findings: Vec<ShadowFinding>,
289}
290
291impl ShadowCompared {
292 #[must_use]
294 pub const fn verdict(&self) -> ShadowVerdict {
295 self.verdict
296 }
297
298 #[must_use]
300 pub const fn coverage(&self) -> &ShadowCoverage {
301 &self.coverage
302 }
303
304 #[must_use]
306 pub fn findings(&self) -> &[ShadowFinding] {
307 &self.findings
308 }
309
310 #[must_use]
314 pub fn is_cutover_evidence(&self) -> bool {
315 self.verdict == ShadowVerdict::Matched && self.coverage.is_complete()
316 }
317}
318
319#[derive(Clone, Copy, Debug, Eq, Ord, PartialEq, PartialOrd)]
321pub enum ShadowUnavailableLane {
322 V1Effective,
324 V2Declared,
326 V2Effective,
328}
329
330#[derive(Clone, Debug, Eq, PartialEq)]
332pub struct ShadowNotCompared {
333 unavailable_lanes: BTreeSet<ShadowUnavailableLane>,
334}
335
336impl ShadowNotCompared {
337 #[must_use]
339 pub const fn unavailable_lanes(&self) -> &BTreeSet<ShadowUnavailableLane> {
340 &self.unavailable_lanes
341 }
342}
343
344#[derive(Clone, Debug, Eq, PartialEq)]
346pub enum ShadowComparison {
347 Compared(ShadowCompared),
349 NotCompared(ShadowNotCompared),
351}
352
353#[derive(Clone, Debug, Eq, PartialEq)]
355pub struct V1ShadowReport {
356 profile: SemanticProfileId,
357 v1_direct: ShadowLaneOutcome,
358 v1_effective: ShadowLaneOutcome,
359 v2_declared: ShadowLaneOutcome,
360 v2_effective: ShadowLaneOutcome,
361 v2_declared_fingerprint: Option<DeclaredIdentityFingerprint>,
362 v2_semantic_fingerprint: Option<SemanticSchemaFingerprint>,
363 comparison: ShadowComparison,
364}
365
366impl V1ShadowReport {
367 #[must_use]
369 pub const fn profile(&self) -> &SemanticProfileId {
370 &self.profile
371 }
372
373 #[must_use]
375 pub const fn v1_direct(&self) -> &ShadowLaneOutcome {
376 &self.v1_direct
377 }
378
379 #[must_use]
381 pub const fn v1_effective(&self) -> &ShadowLaneOutcome {
382 &self.v1_effective
383 }
384
385 #[must_use]
387 pub const fn v2_declared(&self) -> &ShadowLaneOutcome {
388 &self.v2_declared
389 }
390
391 #[must_use]
393 pub const fn v2_effective(&self) -> &ShadowLaneOutcome {
394 &self.v2_effective
395 }
396
397 #[must_use]
399 pub const fn v2_declared_fingerprint(&self) -> Option<&DeclaredIdentityFingerprint> {
400 self.v2_declared_fingerprint.as_ref()
401 }
402
403 #[must_use]
405 pub const fn v2_semantic_fingerprint(&self) -> Option<&SemanticSchemaFingerprint> {
406 self.v2_semantic_fingerprint.as_ref()
407 }
408
409 #[must_use]
411 pub const fn comparison(&self) -> &ShadowComparison {
412 &self.comparison
413 }
414}
415
416#[derive(Clone, Debug, Eq, PartialEq)]
418pub struct V1ShadowInternalError {
419 code: &'static str,
420 message: String,
421}
422
423impl V1ShadowInternalError {
424 fn new(code: &'static str, message: impl Into<String>) -> Self {
425 Self {
426 code,
427 message: message.into(),
428 }
429 }
430
431 #[must_use]
433 pub const fn code(&self) -> &'static str {
434 self.code
435 }
436
437 #[must_use]
439 pub fn message(&self) -> &str {
440 &self.message
441 }
442}
443
444impl fmt::Display for V1ShadowInternalError {
445 fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
446 write!(formatter, "{}: {}", self.code, self.message)
447 }
448}
449
450impl Error for V1ShadowInternalError {}
451
452pub fn v1_shadow_report(
458 typeql: &str,
459 profile: &SemanticProfileId,
460) -> Result<V1ShadowReport, V1ShadowInternalError> {
461 let document = DocumentId::new(SHADOW_DOCUMENT).map_err(|diagnostic| {
462 V1ShadowInternalError::new("shadow_document_id_invalid", diagnostic.to_string())
463 })?;
464
465 let v1_direct_result = parser::parse_typeql(typeql);
466 let v1_direct = match &v1_direct_result {
467 Ok(schema) => accepted_v1(schema),
468 Err(error) => rejected_v1(error),
469 };
470
471 let semantic_profile = SemanticProfile::resolve(profile).map_err(|diagnostic| {
472 V1ShadowInternalError::new("shadow_semantic_profile_invalid", diagnostic.to_string())
473 })?;
474 let v1_effective_result = TypeSchema::from_typeql(typeql);
475 let v1_effective_projection = v1_effective_result
476 .as_ref()
477 .ok()
478 .map(|schema| project_v1(schema, &semantic_profile));
479 let v1_effective = match &v1_effective_result {
480 Ok(schema) => accepted_v1(schema),
481 Err(error) => rejected_v1(error),
482 };
483
484 let v2_declared_result = typeql_to_declared(document, typeql);
485 let v2_declared_fingerprint = v2_declared_result
486 .as_ref()
487 .ok()
488 .map(|declared| declared.declared_identity_fingerprint().clone());
489 let v2_declared = match &v2_declared_result {
490 Ok(declared) => ShadowLaneOutcome::Accepted(ShadowLaneSummary::new(
491 declared
492 .facts()
493 .filter(|fact| matches!(fact, SchemaFact::Type(_)))
494 .count(),
495 )),
496 Err(diagnostics) => rejected_v2(diagnostics),
497 };
498
499 let v2_resolved_result = v2_declared_result
500 .as_ref()
501 .ok()
502 .map(|declared| resolve(declared, profile));
503 let v2_effective_projection = v2_resolved_result
504 .as_ref()
505 .and_then(|result| result.as_ref().ok())
506 .map(project_v2);
507 let v2_semantic_fingerprint = v2_resolved_result
508 .as_ref()
509 .and_then(|result| result.as_ref().ok())
510 .map(|resolved| resolved.semantic_fingerprint().clone());
511 let v2_effective = match &v2_resolved_result {
512 Some(Ok(resolved)) => {
513 ShadowLaneOutcome::Accepted(ShadowLaneSummary::new(resolved.types().len()))
514 }
515 Some(Err(diagnostics)) => rejected_v2(diagnostics),
516 None => ShadowLaneOutcome::NotRun(ShadowLaneNotRun::new("v2_declared_rejected")),
517 };
518
519 let comparison = match (&v1_effective_projection, &v2_effective_projection) {
520 (Some(v1), Some(v2)) => ShadowComparison::Compared(compare(v1, v2)),
521 _ => {
522 let mut unavailable_lanes = BTreeSet::new();
523 if v1_effective_projection.is_none() {
524 unavailable_lanes.insert(ShadowUnavailableLane::V1Effective);
525 }
526 if v2_declared_result.is_err() {
527 unavailable_lanes.insert(ShadowUnavailableLane::V2Declared);
528 }
529 if v2_effective_projection.is_none() {
530 unavailable_lanes.insert(ShadowUnavailableLane::V2Effective);
531 }
532 ShadowComparison::NotCompared(ShadowNotCompared { unavailable_lanes })
533 }
534 };
535
536 Ok(V1ShadowReport {
537 profile: profile.clone(),
538 v1_direct,
539 v1_effective,
540 v2_declared,
541 v2_effective,
542 v2_declared_fingerprint,
543 v2_semantic_fingerprint,
544 comparison,
545 })
546}
547
548#[derive(Clone, Copy, Debug, Eq, PartialEq)]
549enum BasicTypeKind {
550 Entity,
551 Relation,
552 Attribute,
553}
554
555impl BasicTypeKind {
556 const fn as_str(self) -> &'static str {
557 match self {
558 Self::Entity => "entity",
559 Self::Relation => "relation",
560 Self::Attribute => "attribute",
561 }
562 }
563}
564
565#[derive(Clone, Debug, Eq, PartialEq)]
566struct BasicTypeProjection {
567 kind: BasicTypeKind,
568 parent: Option<String>,
569 is_abstract: bool,
570 is_independent: bool,
571 value_type: Option<String>,
572 owns: BTreeMap<String, String>,
573 relates: BTreeMap<String, String>,
574 plays: BTreeMap<String, String>,
575}
576
577#[derive(Clone, Debug, Eq, PartialEq)]
578struct SchemaProjection {
579 types: BTreeMap<String, BasicTypeProjection>,
580 functions: BTreeMap<String, String>,
581 structs: BTreeMap<String, String>,
582 documentation_and_metadata: BTreeMap<String, String>,
583}
584
585fn accepted_v1(schema: &TypeSchema) -> ShadowLaneOutcome {
586 ShadowLaneOutcome::Accepted(ShadowLaneSummary::new(
587 schema.entities.len() + schema.relations.len() + schema.attributes.len(),
588 ))
589}
590
591fn rejected_v1(error: &SchemaError) -> ShadowLaneOutcome {
592 let code = match error {
593 SchemaError::ParseError { .. } => "v1_parse_error",
594 SchemaError::InheritanceCycle { .. } => "v1_inheritance_cycle",
595 SchemaError::UnknownParent { .. } => "v1_unknown_parent",
596 SchemaError::DuplicateDefinition { .. } => "v1_duplicate_definition",
597 SchemaError::ValidationError { .. } => "v1_validation_error",
598 };
599 ShadowLaneOutcome::Rejected(ShadowLaneRejection::new(code, error.to_string()))
600}
601
602fn rejected_v2(diagnostics: &SchemaDiagnostics) -> ShadowLaneOutcome {
603 let code = diagnostics
604 .iter()
605 .next()
606 .map(|entry| entry.diagnostic().code().as_str())
607 .unwrap_or("v2_schema_rejected");
608 ShadowLaneOutcome::Rejected(ShadowLaneRejection::new(code, diagnostics.to_string()))
609}
610
611fn project_v1(schema: &TypeSchema, profile: &SemanticProfile) -> SchemaProjection {
612 let mut types = BTreeMap::new();
613 let mut documentation_and_metadata = BTreeMap::new();
614 for entity in schema.entities.values() {
615 types.insert(
616 entity.name.clone(),
617 BasicTypeProjection {
618 kind: BasicTypeKind::Entity,
619 parent: entity.parent.clone(),
620 is_abstract: entity.is_abstract,
621 is_independent: false,
622 value_type: None,
623 owns: project_v1_owns(&entity.owns, profile),
624 relates: BTreeMap::new(),
625 plays: project_v1_plays(&entity.plays, profile),
626 },
627 );
628 insert_doc_meta(
629 &mut documentation_and_metadata,
630 format!("type {}", entity.name),
631 entity.doc.as_deref(),
632 entity
633 .meta
634 .iter()
635 .map(|(key, value)| (key.as_str(), value.as_str())),
636 );
637 project_v1_interface_docs(
638 &mut documentation_and_metadata,
639 &entity.name,
640 &entity.owns,
641 &entity.plays,
642 &[],
643 );
644 }
645 for relation in schema.relations.values() {
646 types.insert(
647 relation.name.clone(),
648 BasicTypeProjection {
649 kind: BasicTypeKind::Relation,
650 parent: relation.parent.clone(),
651 is_abstract: relation.is_abstract,
652 is_independent: false,
653 value_type: None,
654 owns: project_v1_owns(&relation.owns, profile),
655 relates: project_v1_relates(&relation.roles, profile),
656 plays: project_v1_plays(&relation.plays, profile),
657 },
658 );
659 insert_doc_meta(
660 &mut documentation_and_metadata,
661 format!("type {}", relation.name),
662 relation.doc.as_deref(),
663 relation
664 .meta
665 .iter()
666 .map(|(key, value)| (key.as_str(), value.as_str())),
667 );
668 project_v1_interface_docs(
669 &mut documentation_and_metadata,
670 &relation.name,
671 &relation.owns,
672 &relation.plays,
673 &relation.roles,
674 );
675 }
676 for attribute in schema.attributes.values() {
677 types.insert(
678 attribute.name.clone(),
679 BasicTypeProjection {
680 kind: BasicTypeKind::Attribute,
681 parent: attribute.parent.clone(),
682 is_abstract: attribute.is_abstract,
683 is_independent: attribute.is_independent,
684 value_type: normalize_v1_value_type(&attribute.value_type),
685 owns: BTreeMap::new(),
686 relates: BTreeMap::new(),
687 plays: BTreeMap::new(),
688 },
689 );
690 insert_doc_meta(
691 &mut documentation_and_metadata,
692 format!("type {}", attribute.name),
693 attribute.doc.as_deref(),
694 attribute
695 .meta
696 .iter()
697 .map(|(key, value)| (key.as_str(), value.as_str())),
698 );
699 }
700 SchemaProjection {
701 types,
702 functions: schema
703 .functions
704 .iter()
705 .map(|(name, function)| (name.clone(), project_v1_function(function)))
706 .collect(),
707 structs: schema
708 .structs
709 .iter()
710 .map(|(name, value)| (name.clone(), project_v1_struct(value)))
711 .collect(),
712 documentation_and_metadata,
713 }
714}
715
716fn project_v2(schema: &ResolvedSchema) -> SchemaProjection {
717 let mut documentation_and_metadata = BTreeMap::new();
718 let types = schema
719 .types()
720 .values()
721 .map(|resolved| {
722 let id = resolved.id();
723 let kind = match id.kind() {
724 TypeKind::Entity => BasicTypeKind::Entity,
725 TypeKind::Relation => BasicTypeKind::Relation,
726 TypeKind::Attribute => BasicTypeKind::Attribute,
727 TypeKind::Struct => unreachable!("resolved structs are not schema types"),
728 };
729 let parent = resolved
730 .supertypes()
731 .first()
732 .map(|parent| parent.label().as_str().to_owned());
733 let is_independent = resolved
734 .annotations()
735 .contains_key(&AnnotationKindId::Independent);
736 let value_type = resolved
737 .value_type()
738 .map(|value| v2_value_type(value.value_type()).to_owned());
739 insert_v2_doc_meta(
740 &mut documentation_and_metadata,
741 format!("type {}", id.label().as_str()),
742 resolved.annotations(),
743 );
744 for owns in resolved.owns().values() {
745 insert_v2_doc_meta(
746 &mut documentation_and_metadata,
747 format!(
748 "owns {} {}",
749 id.label().as_str(),
750 owns.id().attribute().label().as_str()
751 ),
752 owns.annotations(),
753 );
754 }
755 for plays in resolved.plays().values() {
756 insert_v2_doc_meta(
757 &mut documentation_and_metadata,
758 format!(
759 "plays {} {}:{}",
760 id.label().as_str(),
761 plays.id().role().declaring_relation().as_str(),
762 plays.id().role().label().as_str()
763 ),
764 plays.annotations(),
765 );
766 }
767 for relates in resolved.relates().values() {
768 insert_v2_doc_meta(
769 &mut documentation_and_metadata,
770 format!(
771 "relates {} {}",
772 id.label().as_str(),
773 relates.id().role().label().as_str()
774 ),
775 relates.annotations(),
776 );
777 }
778 (
779 id.label().as_str().to_owned(),
780 BasicTypeProjection {
781 kind,
782 parent,
783 is_abstract: resolved.is_abstract(),
784 is_independent,
785 value_type,
786 owns: resolved
787 .owns()
788 .values()
789 .map(|owns| {
790 (
791 owns.id().attribute().label().as_str().to_owned(),
792 format_owns(owns.cardinality(), owns.is_key(), owns.is_unique()),
793 )
794 })
795 .collect(),
796 relates: resolved
797 .relates()
798 .values()
799 .map(|relates| {
800 let replaced = relates
801 .replaced_roles()
802 .iter()
803 .map(|role| role.label().as_str())
804 .collect::<Vec<_>>()
805 .join(",");
806 (
807 relates.id().role().label().as_str().to_owned(),
808 format_relates(
809 relates.cardinality(),
810 relates.is_abstract(),
811 &replaced,
812 ),
813 )
814 })
815 .collect(),
816 plays: resolved
817 .plays()
818 .values()
819 .map(|plays| {
820 (
821 format!(
822 "{}:{}",
823 plays.id().role().declaring_relation().as_str(),
824 plays.id().role().label().as_str()
825 ),
826 format_cardinality(plays.cardinality()),
827 )
828 })
829 .collect(),
830 },
831 )
832 })
833 .collect();
834 SchemaProjection {
835 types,
836 functions: schema
837 .functions()
838 .values()
839 .map(|function| {
840 (
841 function.id().label().as_str().to_owned(),
842 project_v2_function(function.declaration()),
843 )
844 })
845 .collect(),
846 structs: schema
847 .structs()
848 .values()
849 .map(|value| {
850 (
851 value.id().label().as_str().to_owned(),
852 value
853 .fields()
854 .iter()
855 .map(|field| {
856 format!(
857 "{}:{}{}",
858 field.name().as_str(),
859 v2_value_type(field.value_type()),
860 if field.optional() { "?" } else { "" }
861 )
862 })
863 .collect::<Vec<_>>()
864 .join(","),
865 )
866 })
867 .collect(),
868 documentation_and_metadata,
869 }
870}
871
872fn compare(v1: &SchemaProjection, v2: &SchemaProjection) -> ShadowCompared {
873 let coverage = coverage();
874 let labels = v1
875 .types
876 .keys()
877 .chain(v2.types.keys())
878 .cloned()
879 .collect::<BTreeSet<_>>();
880 let mut findings = BTreeSet::new();
881 for label in labels {
882 match (v1.types.get(&label), v2.types.get(&label)) {
883 (Some(left), Some(right)) => {
884 if left.kind != right.kind {
885 findings.insert(ShadowFinding::new(
886 ShadowDimension::TypeKind,
887 &label,
888 Some(left.kind.as_str().to_owned()),
889 Some(right.kind.as_str().to_owned()),
890 ));
891 }
892 insert_difference(
893 &mut findings,
894 ShadowDimension::DirectParent,
895 &label,
896 left.parent.clone(),
897 right.parent.clone(),
898 );
899 insert_difference(
900 &mut findings,
901 ShadowDimension::TypeAbstract,
902 &label,
903 Some(left.is_abstract.to_string()),
904 Some(right.is_abstract.to_string()),
905 );
906 insert_difference(
907 &mut findings,
908 ShadowDimension::AttributeIndependent,
909 &label,
910 Some(left.is_independent.to_string()),
911 Some(right.is_independent.to_string()),
912 );
913 insert_difference(
914 &mut findings,
915 ShadowDimension::ValueType,
916 &label,
917 left.value_type.clone(),
918 right.value_type.clone(),
919 );
920 compare_named_values(
921 &mut findings,
922 ShadowDimension::EffectiveOwns,
923 &format!("{label} owns "),
924 &left.owns,
925 &right.owns,
926 );
927 compare_named_values(
928 &mut findings,
929 ShadowDimension::EffectiveRelates,
930 &format!("{label} relates "),
931 &left.relates,
932 &right.relates,
933 );
934 compare_named_values(
935 &mut findings,
936 ShadowDimension::EffectivePlays,
937 &format!("{label} plays "),
938 &left.plays,
939 &right.plays,
940 );
941 }
942 (Some(left), None) => {
943 findings.insert(ShadowFinding::new(
944 ShadowDimension::TypeExistence,
945 &label,
946 Some(left.kind.as_str().to_owned()),
947 None,
948 ));
949 }
950 (None, Some(right)) => {
951 findings.insert(ShadowFinding::new(
952 ShadowDimension::TypeExistence,
953 &label,
954 None,
955 Some(right.kind.as_str().to_owned()),
956 ));
957 }
958 (None, None) => {}
959 }
960 }
961 compare_named_values(
962 &mut findings,
963 ShadowDimension::FunctionSignatures,
964 "function ",
965 &v1.functions,
966 &v2.functions,
967 );
968 compare_named_values(
969 &mut findings,
970 ShadowDimension::StructFields,
971 "struct ",
972 &v1.structs,
973 &v2.structs,
974 );
975 compare_named_values(
976 &mut findings,
977 ShadowDimension::DocumentationAndMetadata,
978 "",
979 &v1.documentation_and_metadata,
980 &v2.documentation_and_metadata,
981 );
982 let findings = findings.into_iter().collect::<Vec<_>>();
983 ShadowCompared {
984 verdict: if findings.is_empty() {
985 ShadowVerdict::Matched
986 } else {
987 ShadowVerdict::Mismatched
988 },
989 coverage,
990 findings,
991 }
992}
993
994fn compare_named_values(
995 findings: &mut BTreeSet<ShadowFinding>,
996 dimension: ShadowDimension,
997 subject_prefix: &str,
998 v1: &BTreeMap<String, String>,
999 v2: &BTreeMap<String, String>,
1000) {
1001 for key in v1.keys().chain(v2.keys()).collect::<BTreeSet<_>>() {
1002 insert_difference(
1003 findings,
1004 dimension,
1005 &format!("{subject_prefix}{key}"),
1006 v1.get(key).cloned(),
1007 v2.get(key).cloned(),
1008 );
1009 }
1010}
1011
1012fn insert_difference(
1013 findings: &mut BTreeSet<ShadowFinding>,
1014 dimension: ShadowDimension,
1015 label: &str,
1016 v1_value: Option<String>,
1017 v2_value: Option<String>,
1018) {
1019 if v1_value != v2_value {
1020 findings.insert(ShadowFinding::new(dimension, label, v1_value, v2_value));
1021 }
1022}
1023
1024fn coverage() -> ShadowCoverage {
1025 let compared = BTreeSet::from([
1026 ShadowDimension::TypeExistence,
1027 ShadowDimension::TypeKind,
1028 ShadowDimension::DirectParent,
1029 ShadowDimension::TypeAbstract,
1030 ShadowDimension::AttributeIndependent,
1031 ShadowDimension::ValueType,
1032 ShadowDimension::EffectiveOwns,
1033 ShadowDimension::EffectiveRelates,
1034 ShadowDimension::EffectivePlays,
1035 ShadowDimension::DocumentationAndMetadata,
1036 ShadowDimension::FunctionSignatures,
1037 ]);
1038 let unimplemented = BTreeSet::new();
1039 let not_representable = BTreeSet::from([
1040 ShadowDimension::FunctionBodiesAndAnnotations,
1041 ShadowDimension::StructFields,
1042 ShadowDimension::SourceCommentsAndSpans,
1043 ShadowDimension::OmittedVersusExplicitIdentity,
1044 ShadowDimension::IndependentAnnotationIdentityAndRemoval,
1045 ShadowDimension::SubAnnotations,
1046 ShadowDimension::ExtensionsAndCapabilities,
1047 ShadowDimension::ResolverGraphsAndOrigins,
1048 ShadowDimension::CardinalityOutsideV1U32,
1049 ]);
1050 let blind_spots = unimplemented.union(¬_representable).copied().collect();
1051 ShadowCoverage {
1052 compared,
1053 unimplemented,
1054 not_representable,
1055 blind_spots,
1056 }
1057}
1058
1059fn project_v1_owns(owns: &[OwnedAttribute], profile: &SemanticProfile) -> BTreeMap<String, String> {
1060 owns.iter()
1061 .map(|owns| {
1062 let cardinality = if owns.is_key {
1063 V2Cardinality::new(1, Some(1)).expect("the effective key cardinality is exact-one")
1064 } else {
1065 v1_cardinality(owns.cardinality.as_ref(), profile, InterfaceKind::Owns)
1066 };
1067 (
1068 owns.name.clone(),
1069 format_owns(cardinality, owns.is_key, owns.is_key || owns.is_unique),
1070 )
1071 })
1072 .collect()
1073}
1074
1075fn project_v1_plays(plays: &[PlayedRole], profile: &SemanticProfile) -> BTreeMap<String, String> {
1076 plays
1077 .iter()
1078 .map(|plays| {
1079 (
1080 plays.role_ref.clone(),
1081 format_cardinality(v1_cardinality(
1082 plays.cardinality.as_ref(),
1083 profile,
1084 InterfaceKind::Plays,
1085 )),
1086 )
1087 })
1088 .collect()
1089}
1090
1091fn project_v1_relates(roles: &[RoleSpec], profile: &SemanticProfile) -> BTreeMap<String, String> {
1092 roles
1093 .iter()
1094 .map(|role| {
1095 (
1096 role.name.clone(),
1097 format_relates(
1098 v1_cardinality(role.cardinality.as_ref(), profile, InterfaceKind::Relates),
1099 role.is_abstract,
1100 role.overrides.as_deref().unwrap_or(""),
1101 ),
1102 )
1103 })
1104 .collect()
1105}
1106
1107fn v1_cardinality(
1108 cardinality: Option<&V1Cardinality>,
1109 profile: &SemanticProfile,
1110 interface: InterfaceKind,
1111) -> V2Cardinality {
1112 cardinality.map_or_else(
1113 || profile.default_cardinality(interface),
1114 |cardinality| {
1115 V2Cardinality::new(u64::from(cardinality.min), cardinality.max.map(u64::from))
1116 .expect("validated V1 cardinality must fit the V2 domain")
1117 },
1118 )
1119}
1120
1121fn format_cardinality(cardinality: V2Cardinality) -> String {
1122 format!(
1123 "{}..{}",
1124 cardinality.min(),
1125 cardinality
1126 .max()
1127 .map_or_else(|| "unbounded".to_owned(), |maximum| maximum.to_string())
1128 )
1129}
1130
1131fn format_owns(cardinality: V2Cardinality, key: bool, unique: bool) -> String {
1132 format!(
1133 "card={};key={key};unique={unique}",
1134 format_cardinality(cardinality)
1135 )
1136}
1137
1138fn format_relates(cardinality: V2Cardinality, is_abstract: bool, replaces: &str) -> String {
1139 format!(
1140 "card={};abstract={is_abstract};replaces={replaces}",
1141 format_cardinality(cardinality)
1142 )
1143}
1144
1145fn project_v1_interface_docs(
1146 projection: &mut BTreeMap<String, String>,
1147 owner: &str,
1148 owns: &[OwnedAttribute],
1149 plays: &[PlayedRole],
1150 relates: &[RoleSpec],
1151) {
1152 for interface in owns {
1153 insert_doc_meta(
1154 projection,
1155 format!("owns {owner} {}", interface.name),
1156 interface.doc.as_deref(),
1157 interface
1158 .meta
1159 .iter()
1160 .map(|(key, value)| (key.as_str(), value.as_str())),
1161 );
1162 }
1163 for interface in plays {
1164 insert_doc_meta(
1165 projection,
1166 format!("plays {owner} {}", interface.role_ref),
1167 interface.doc.as_deref(),
1168 interface
1169 .meta
1170 .iter()
1171 .map(|(key, value)| (key.as_str(), value.as_str())),
1172 );
1173 }
1174 for interface in relates {
1175 insert_doc_meta(
1176 projection,
1177 format!("relates {owner} {}", interface.name),
1178 interface.doc.as_deref(),
1179 interface
1180 .meta
1181 .iter()
1182 .map(|(key, value)| (key.as_str(), value.as_str())),
1183 );
1184 }
1185}
1186
1187fn insert_doc_meta<'a>(
1188 projection: &mut BTreeMap<String, String>,
1189 subject: String,
1190 doc: Option<&str>,
1191 meta: impl Iterator<Item = (&'a str, &'a str)>,
1192) {
1193 let meta = meta
1194 .map(|(key, value)| format!("{key:?}={value:?}"))
1195 .collect::<Vec<_>>()
1196 .join(",");
1197 if doc.is_some() || !meta.is_empty() {
1198 projection.insert(subject, format!("doc={doc:?};meta={meta}"));
1199 }
1200}
1201
1202fn insert_v2_doc_meta(
1203 projection: &mut BTreeMap<String, String>,
1204 subject: String,
1205 annotations: &BTreeMap<AnnotationKindId, SchemaAnnotationValue>,
1206) {
1207 let doc = annotations.get(&AnnotationKindId::Doc).and_then(|value| {
1208 if let SchemaAnnotationValue::Doc(doc) = value {
1209 Some(doc.as_str())
1210 } else {
1211 None
1212 }
1213 });
1214 let meta = annotations.iter().filter_map(|(kind, value)| {
1215 let AnnotationKindId::Meta(key) = kind else {
1216 return None;
1217 };
1218 let value = match value {
1219 SchemaAnnotationValue::Meta(CanonicalValue::String(value)) => value.as_str().to_owned(),
1220 SchemaAnnotationValue::Meta(value) => format!("{value:?}"),
1221 _ => return None,
1222 };
1223 Some((key.as_str(), value))
1224 });
1225 let meta = meta.collect::<Vec<_>>();
1226 insert_doc_meta(
1227 projection,
1228 subject,
1229 doc,
1230 meta.iter().map(|(key, value)| (*key, value.as_str())),
1231 );
1232}
1233
1234fn project_v1_function(function: &V1Function) -> String {
1235 let parameters = function
1236 .parameters
1237 .iter()
1238 .map(|parameter| {
1239 format!(
1240 "{}:{}",
1241 parameter.name,
1242 normalize_type_token(¶meter.type_)
1243 )
1244 })
1245 .collect::<Vec<_>>()
1246 .join(",");
1247 let returns = function
1248 .return_type
1249 .types
1250 .iter()
1251 .map(|value| {
1252 format!(
1253 "{}{}",
1254 normalize_type_token(&value.name),
1255 if value.optional { "?" } else { "" }
1256 )
1257 })
1258 .collect::<Vec<_>>()
1259 .join(",");
1260 let mode = if function.return_type.is_stream {
1261 "stream"
1262 } else if function.return_type.types.len() == 1 {
1263 "scalar"
1264 } else {
1265 "tuple"
1266 };
1267 format!("({parameters})->{mode}({returns})")
1268}
1269
1270fn project_v2_function(function: &type_bridge_contract::schema::FunctionFact) -> String {
1271 let signature = function.signature();
1272 let parameters = signature
1273 .parameters()
1274 .iter()
1275 .map(|parameter| {
1276 format!(
1277 "{}:{}",
1278 parameter.name().as_str(),
1279 type_reference(parameter.type_ref())
1280 )
1281 })
1282 .collect::<Vec<_>>()
1283 .join(",");
1284 let (mode, elements) = match signature.returns() {
1285 FunctionReturnMode::Scalar(element) => ("scalar", std::slice::from_ref(element)),
1286 FunctionReturnMode::Tuple(elements) => ("tuple", elements.as_slice()),
1287 FunctionReturnMode::Stream(elements) => ("stream", elements.as_slice()),
1288 };
1289 let returns = elements
1290 .iter()
1291 .map(|element| {
1292 format!(
1293 "{}{}",
1294 type_reference(element.type_ref()),
1295 if element.optional() { "?" } else { "" }
1296 )
1297 })
1298 .collect::<Vec<_>>()
1299 .join(",");
1300 format!("({parameters})->{mode}({returns})")
1301}
1302
1303fn type_reference(reference: &TypeReference) -> String {
1304 match reference {
1305 TypeReference::Value(value) => v2_value_type(*value).to_owned(),
1306 TypeReference::Schema(label) => label.as_str().to_owned(),
1307 }
1308}
1309
1310fn project_v1_struct(value: &V1Struct) -> String {
1311 value
1312 .fields
1313 .iter()
1314 .map(|field| {
1315 format!(
1316 "{}:{}{}",
1317 field.name,
1318 normalize_type_token(&field.value_type),
1319 if field.optional { "?" } else { "" }
1320 )
1321 })
1322 .collect::<Vec<_>>()
1323 .join(",")
1324}
1325
1326fn normalize_type_token(value: &str) -> String {
1327 normalize_v1_value_type(value).unwrap_or_else(|| value.trim().to_owned())
1328}
1329
1330fn normalize_v1_value_type(value: &str) -> Option<String> {
1331 let normalized = match value.trim() {
1332 "" => return None,
1333 "long" | "integer" => "integer",
1334 "bool" | "boolean" => "boolean",
1335 "datetime-tz" | "datetime_tz" => "datetime-tz",
1336 other => other,
1337 };
1338 Some(normalized.to_owned())
1339}
1340
1341const fn v2_value_type(value: ValueTypeTag) -> &'static str {
1342 match value {
1343 ValueTypeTag::String => "string",
1344 ValueTypeTag::Long => "integer",
1345 ValueTypeTag::Double => "double",
1346 ValueTypeTag::Boolean => "boolean",
1347 ValueTypeTag::Date => "date",
1348 ValueTypeTag::DateTime => "datetime",
1349 ValueTypeTag::DateTimeTz => "datetime-tz",
1350 ValueTypeTag::Decimal => "decimal",
1351 ValueTypeTag::Duration => "duration",
1352 }
1353}