1use std::collections::BTreeMap;
4
5use type_bridge_contract::capability::CapabilitySet;
6use type_bridge_contract::codec::FormatVersion;
7use type_bridge_contract::diagnostic::{Diagnostic, DiagnosticCategory, DiagnosticCode};
8use type_bridge_contract::fingerprint::SemanticProfileId;
9use type_bridge_contract::schema::{
10 AnnotationKindId, DeclaredIdentityFingerprint, DeclaredSchema, DocumentId,
11 SchemaAnnotationValue, SchemaDiagnostic, SchemaDiagnostics, SchemaFact, SchemaFactId,
12 SourceSpan, SourcedSchemaFact,
13};
14use type_bridge_contract::semantic_profile::{InterfaceKind, SemanticProfile};
15
16use crate::ManagedSchemaScope;
17
18pub const OBSERVED_SCHEMA_CANONICALIZATION_VERSION: &str = "typebridge.observed-schema/v1";
20
21#[derive(Clone, Debug, Eq, PartialEq)]
23pub enum ObservedFactProvenance {
24 Direct,
26 Inherited {
28 declared_fact: SchemaFactId,
30 },
31 ServerDefault,
33 Ambiguous,
35}
36
37#[derive(Clone, Copy, Debug, Eq, PartialEq)]
39pub enum ObservedFactScope {
40 Managed,
42 TypeBridgeInternal,
44}
45
46#[derive(Clone, Debug, Eq, PartialEq)]
48pub struct ObservedSchemaFact {
49 fact: SchemaFact,
50 provenance: ObservedFactProvenance,
51 scope: ObservedFactScope,
52}
53
54impl ObservedSchemaFact {
55 pub const fn new(
57 fact: SchemaFact,
58 provenance: ObservedFactProvenance,
59 scope: ObservedFactScope,
60 ) -> Self {
61 Self {
62 fact,
63 provenance,
64 scope,
65 }
66 }
67
68 pub const fn fact(&self) -> &SchemaFact {
70 &self.fact
71 }
72
73 pub const fn provenance(&self) -> &ObservedFactProvenance {
75 &self.provenance
76 }
77
78 pub const fn scope(&self) -> ObservedFactScope {
80 self.scope
81 }
82}
83
84#[derive(Clone, Debug, Eq, PartialEq)]
86pub struct ObservedSchema {
87 format: FormatVersion,
88 required_capabilities: CapabilitySet,
89 facts: Vec<ObservedSchemaFact>,
90}
91
92impl ObservedSchema {
93 #[must_use]
95 pub fn new(
96 format: FormatVersion,
97 required_capabilities: CapabilitySet,
98 facts: impl IntoIterator<Item = ObservedSchemaFact>,
99 ) -> Self {
100 Self {
101 format,
102 required_capabilities,
103 facts: facts.into_iter().collect(),
104 }
105 }
106
107 pub const fn format(&self) -> FormatVersion {
109 self.format
110 }
111
112 pub const fn required_capabilities(&self) -> &CapabilitySet {
114 &self.required_capabilities
115 }
116
117 pub fn facts(&self) -> impl ExactSizeIterator<Item = &ObservedSchemaFact> {
119 self.facts.iter()
120 }
121}
122
123#[derive(Clone, Debug, Eq, PartialEq)]
125pub struct CanonicalObservedSchema {
126 direct_schema: DeclaredSchema,
127 managed_scope: ManagedSchemaScope,
128 semantic_profile: SemanticProfileId,
129}
130
131impl CanonicalObservedSchema {
132 pub const fn canonicalization_version(&self) -> &'static str {
134 OBSERVED_SCHEMA_CANONICALIZATION_VERSION
135 }
136
137 pub const fn direct_schema(&self) -> &DeclaredSchema {
142 &self.direct_schema
143 }
144
145 pub const fn managed_scope(&self) -> &ManagedSchemaScope {
147 &self.managed_scope
148 }
149
150 pub const fn semantic_profile(&self) -> &SemanticProfileId {
152 &self.semantic_profile
153 }
154
155 pub fn canonical_identity_bytes(&self) -> Result<Vec<u8>, Diagnostic> {
157 self.direct_schema.canonical_identity_bytes()
158 }
159
160 pub const fn declared_identity_fingerprint(&self) -> &DeclaredIdentityFingerprint {
162 self.direct_schema.declared_identity_fingerprint()
163 }
164}
165
166pub fn canonicalize_observed_schema(
171 observed: &ObservedSchema,
172 semantic_profile: &SemanticProfile,
173) -> Result<CanonicalObservedSchema, SchemaDiagnostics> {
174 let mut captures = BTreeMap::<SchemaFactId, Vec<&ObservedSchemaFact>>::new();
175 for captured in observed.facts() {
176 captures
177 .entry(captured.fact().id())
178 .or_default()
179 .push(captured);
180 }
181
182 let mut diagnostics = Vec::new();
183 let mut direct = BTreeMap::<SchemaFactId, (&SchemaFact, ObservedFactScope)>::new();
184 let mut inherited_origins = Vec::<SchemaFactId>::new();
185 for (id, entries) in captures {
186 if entries.len() != 1 {
187 diagnostics.push(observed_diagnostic(
188 "duplicate_observed_fact",
189 "provider introspection returned one fact identity more than once",
190 ));
191 continue;
192 }
193
194 let captured = entries[0];
195 match captured.provenance() {
196 ObservedFactProvenance::Direct => {
197 direct.insert(id, (captured.fact(), captured.scope()));
198 }
199 ObservedFactProvenance::Inherited { declared_fact } => {
200 inherited_origins.push(declared_fact.clone());
201 }
202 ObservedFactProvenance::ServerDefault => {
203 if !is_server_default_cardinality(captured.fact(), semantic_profile) {
204 diagnostics.push(observed_diagnostic(
205 "invalid_observed_server_default",
206 "a server default must exactly match the selected profile's omitted interface cardinality",
207 ));
208 }
209 }
210 ObservedFactProvenance::Ambiguous => diagnostics.push(observed_diagnostic(
211 "ambiguous_observed_provenance",
212 "direct, inherited, and synthesized provenance could not be recovered unambiguously",
213 )),
214 }
215 }
216
217 for declared_fact in inherited_origins {
218 if !direct.contains_key(&declared_fact) {
219 diagnostics.push(observed_diagnostic(
220 "invalid_observed_inheritance_origin",
221 "an inherited fact must identify an existing direct declaration",
222 ));
223 }
224 }
225
226 if !diagnostics.is_empty() {
227 return Err(SchemaDiagnostics::from_vec(diagnostics));
228 }
229
230 let synthetic_source = synthetic_observed_source()?;
231 let managed_scope = ManagedSchemaScope::new(
232 direct
233 .iter()
234 .filter(|&(_, (_, scope))| *scope == ObservedFactScope::Managed)
235 .map(|(id, _)| id.clone()),
236 );
237 let direct_schema = DeclaredSchema::from_facts(
238 observed.format(),
239 observed.required_capabilities().clone(),
240 direct
241 .into_values()
242 .map(|(fact, _)| SourcedSchemaFact::new(fact.clone(), synthetic_source.clone())),
243 )?;
244
245 Ok(CanonicalObservedSchema {
246 direct_schema,
247 managed_scope,
248 semantic_profile: semantic_profile.id().clone(),
249 })
250}
251
252fn is_server_default_cardinality(fact: &SchemaFact, profile: &SemanticProfile) -> bool {
253 let SchemaFact::Annotation(annotation) = fact else {
254 return false;
255 };
256 if annotation.id().kind() != &AnnotationKindId::Card {
257 return false;
258 }
259 let kind = match annotation.id().subject() {
260 type_bridge_contract::schema::AnnotationSubjectId::Owns(_) => InterfaceKind::Owns,
261 type_bridge_contract::schema::AnnotationSubjectId::Relates(_) => InterfaceKind::Relates,
262 type_bridge_contract::schema::AnnotationSubjectId::Plays(_) => InterfaceKind::Plays,
263 _ => return false,
264 };
265 matches!(
266 annotation.value(),
267 SchemaAnnotationValue::Cardinality(cardinality)
268 if *cardinality == profile.default_cardinality(kind)
269 )
270}
271
272fn synthetic_observed_source() -> Result<SourceSpan, SchemaDiagnostics> {
273 let document = DocumentId::new("observed.schema").map_err(schema_diagnostics)?;
274 SourceSpan::new(document, 0, 1, 1, 1, 1, 2).map_err(schema_diagnostics)
275}
276
277fn observed_diagnostic(code: &'static str, message: &'static str) -> SchemaDiagnostic {
278 SchemaDiagnostic::new(
279 Diagnostic::new(
280 DiagnosticCategory::InvalidContract,
281 DiagnosticCode::new(code).expect("static observed-schema diagnostic code is valid"),
282 message,
283 ),
284 None,
285 )
286}
287
288fn schema_diagnostics(diagnostic: Diagnostic) -> SchemaDiagnostics {
289 SchemaDiagnostics::one(SchemaDiagnostic::new(diagnostic, None))
290}