Skip to main content

type_bridge_schema/
observed.rs

1//! Pure canonicalization of already-captured provider introspection.
2
3use 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
18/// Version of the provider-introspection-to-direct-facts canonicalization policy.
19pub const OBSERVED_SCHEMA_CANONICALIZATION_VERSION: &str = "typebridge.observed-schema/v1";
20
21/// Provider evidence describing why an introspected fact is visible.
22#[derive(Clone, Debug, Eq, PartialEq)]
23pub enum ObservedFactProvenance {
24    /// The fact was declared directly on its observed subject.
25    Direct,
26    /// The fact is an effective projection of another direct declaration.
27    Inherited {
28        /// Stable identity of the declaration from which the fact was inherited.
29        declared_fact: SchemaFactId,
30    },
31    /// The provider synthesized an omitted default rather than observing a declaration.
32    ServerDefault,
33    /// The provider could not distinguish direct, inherited, and synthesized state.
34    Ambiguous,
35}
36
37/// Deployment ownership assigned while capturing one introspected fact.
38#[derive(Clone, Copy, Debug, Eq, PartialEq)]
39pub enum ObservedFactScope {
40    /// The direct fact belongs to the deployment's managed schema scope.
41    Managed,
42    /// The direct fact is TypeBridge-owned runtime infrastructure, not user schema.
43    TypeBridgeInternal,
44}
45
46/// One validated contract fact paired with provider provenance and deployment scope.
47#[derive(Clone, Debug, Eq, PartialEq)]
48pub struct ObservedSchemaFact {
49    fact: SchemaFact,
50    provenance: ObservedFactProvenance,
51    scope: ObservedFactScope,
52}
53
54impl ObservedSchemaFact {
55    /// Capture one provider fact without interpreting its provenance yet.
56    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    /// Return the captured contract fact.
69    pub const fn fact(&self) -> &SchemaFact {
70        &self.fact
71    }
72
73    /// Return the provider provenance classification.
74    pub const fn provenance(&self) -> &ObservedFactProvenance {
75        &self.provenance
76    }
77
78    /// Return the deployment ownership classification.
79    pub const fn scope(&self) -> ObservedFactScope {
80        self.scope
81    }
82}
83
84/// An immutable provider-introspection capture with no server or network behavior.
85#[derive(Clone, Debug, Eq, PartialEq)]
86pub struct ObservedSchema {
87    format: FormatVersion,
88    required_capabilities: CapabilitySet,
89    facts: Vec<ObservedSchemaFact>,
90}
91
92impl ObservedSchema {
93    /// Construct a raw capture for later fail-closed canonicalization.
94    #[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    /// Return the owning schema format version.
108    pub const fn format(&self) -> FormatVersion {
109        self.format
110    }
111
112    /// Return capabilities required by the captured schema.
113    pub const fn required_capabilities(&self) -> &CapabilitySet {
114        &self.required_capabilities
115    }
116
117    /// Iterate captured facts in provider capture order.
118    pub fn facts(&self) -> impl ExactSizeIterator<Item = &ObservedSchemaFact> {
119        self.facts.iter()
120    }
121}
122
123/// Comparable direct schema and managed scope reconstructed from introspection.
124#[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    /// Return the exact observed canonicalization policy version.
133    pub const fn canonicalization_version(&self) -> &'static str {
134        OBSERVED_SCHEMA_CANONICALIZATION_VERSION
135    }
136
137    /// Return the reconstructed direct schema.
138    ///
139    /// Its source spans identify synthetic observed input because provider
140    /// introspection has no source-document coordinates.
141    pub const fn direct_schema(&self) -> &DeclaredSchema {
142        &self.direct_schema
143    }
144
145    /// Return the reconstructed managed direct-fact scope.
146    pub const fn managed_scope(&self) -> &ManagedSchemaScope {
147        &self.managed_scope
148    }
149
150    /// Return the profile used to classify synthesized provider defaults.
151    pub const fn semantic_profile(&self) -> &SemanticProfileId {
152        &self.semantic_profile
153    }
154
155    /// Return canonical direct-identity bytes comparable with authored schemas.
156    pub fn canonical_identity_bytes(&self) -> Result<Vec<u8>, Diagnostic> {
157        self.direct_schema.canonical_identity_bytes()
158    }
159
160    /// Return the authored-schema-compatible direct-identity fingerprint.
161    pub const fn declared_identity_fingerprint(&self) -> &DeclaredIdentityFingerprint {
162        self.direct_schema.declared_identity_fingerprint()
163    }
164}
165
166/// Canonicalize one captured provider schema without performing provider I/O.
167///
168/// Direct facts are retained, effective inherited facts and proven synthesized
169/// cardinality defaults are removed, and any ambiguous provenance fails closed.
170pub 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}