Skip to main content

omena_bridge/
source_evidence.rs

1use std::collections::{BTreeMap, BTreeSet};
2
3use engine_input_producers::{
4    EngineInputV2, ExpressionSemanticsEvaluatorCandidatePayloadV0,
5    summarize_expression_semantics_evaluator_candidates_input,
6    summarize_selector_usage_evaluator_candidates_input,
7};
8use serde::Serialize;
9
10#[derive(Debug, Clone, PartialEq, Eq, Serialize)]
11#[serde(rename_all = "camelCase")]
12pub struct SourceInputPromotionEvidenceSummaryV0 {
13    pub schema_version: &'static str,
14    pub product: &'static str,
15    pub input_version: String,
16    pub reference_site_identity: ReferenceSiteIdentityEvidenceV0,
17    pub certainty_reason: CertaintyReasonEvidenceV0,
18    pub binding_origin: BindingOriginEvidenceV0,
19    pub style_module_edge: StyleModuleEdgeEvidenceV0,
20    pub value_domain_explanation: ValueDomainExplanationEvidenceV0,
21    pub blocking_gaps: Vec<&'static str>,
22    pub next_priorities: Vec<&'static str>,
23}
24
25#[derive(Debug, Clone, PartialEq, Eq, Serialize)]
26#[serde(rename_all = "camelCase")]
27pub struct ReferenceSiteIdentityEvidenceV0 {
28    pub status: &'static str,
29    pub selector_count: usize,
30    pub reference_site_count: usize,
31    pub direct_reference_site_count: usize,
32    pub expanded_reference_site_count: usize,
33    pub style_dependency_reference_site_count: usize,
34    pub editable_direct_site_count: usize,
35    pub reference_kind_counts: BTreeMap<String, usize>,
36}
37
38#[derive(Debug, Clone, PartialEq, Eq, Serialize)]
39#[serde(rename_all = "camelCase")]
40pub struct CertaintyReasonEvidenceV0 {
41    pub status: &'static str,
42    pub expression_count: usize,
43    pub exact_count: usize,
44    pub inferred_count: usize,
45    pub possible_count: usize,
46    pub missing_reason_count: usize,
47    pub reason_counts: BTreeMap<String, usize>,
48    pub shape_kind_counts: BTreeMap<String, usize>,
49    pub shape_label_counts: BTreeMap<String, usize>,
50}
51
52#[derive(Debug, Clone, PartialEq, Eq, Serialize)]
53#[serde(rename_all = "camelCase")]
54pub struct BindingOriginEvidenceV0 {
55    pub status: &'static str,
56    pub expression_count: usize,
57    pub direct_class_name_count: usize,
58    pub root_binding_count: usize,
59    pub access_path_count: usize,
60    pub access_path_segment_count: usize,
61    pub expression_kind_counts: BTreeMap<String, usize>,
62}
63
64#[derive(Debug, Clone, PartialEq, Eq, Serialize)]
65#[serde(rename_all = "camelCase")]
66pub struct StyleModuleEdgeEvidenceV0 {
67    pub status: &'static str,
68    pub source_style_edge_count: usize,
69    pub distinct_style_module_count: usize,
70    pub missing_style_document_edge_count: usize,
71    pub composed_edge_count: usize,
72    pub imported_composed_edge_count: usize,
73    pub global_composed_edge_count: usize,
74}
75
76#[derive(Debug, Clone, PartialEq, Eq, Serialize)]
77#[serde(rename_all = "camelCase")]
78pub struct ValueDomainExplanationEvidenceV0 {
79    pub status: &'static str,
80    pub expression_count: usize,
81    pub exact_expression_count: usize,
82    pub finite_value_expression_count: usize,
83    pub constrained_expression_count: usize,
84    pub unknown_expression_count: usize,
85    pub finite_value_count: usize,
86    pub value_domain_kind_counts: BTreeMap<String, usize>,
87    pub constraint_kind_counts: BTreeMap<String, usize>,
88}
89
90pub fn summarize_omena_bridge_source_input_evidence(
91    input: &EngineInputV2,
92) -> SourceInputPromotionEvidenceSummaryV0 {
93    let reference_site_identity = summarize_reference_site_identity(input);
94    let certainty_reason = summarize_certainty_reason(input);
95    let binding_origin = summarize_binding_origin(input);
96    let style_module_edge = summarize_style_module_edge(input);
97    let value_domain_explanation = summarize_value_domain_explanation(input);
98    let mut blocking_gaps = Vec::new();
99
100    if reference_site_identity.status == "gap" {
101        blocking_gaps.push("referenceSiteIdentity");
102    }
103    if certainty_reason.status == "gap" {
104        blocking_gaps.push("certaintyReason");
105    }
106    if binding_origin.status == "gap" {
107        blocking_gaps.push("bindingOrigin");
108    }
109    if style_module_edge.status == "gap" {
110        blocking_gaps.push("styleModuleEdge");
111    }
112    if value_domain_explanation.status == "gap" {
113        blocking_gaps.push("valueDomainExplanation");
114    }
115
116    SourceInputPromotionEvidenceSummaryV0 {
117        schema_version: "0",
118        product: "omena-semantic.source-input-evidence",
119        input_version: input.version.clone(),
120        reference_site_identity,
121        certainty_reason,
122        binding_origin,
123        style_module_edge,
124        value_domain_explanation,
125        blocking_gaps,
126        next_priorities: Vec::new(),
127    }
128}
129
130fn summarize_reference_site_identity(input: &EngineInputV2) -> ReferenceSiteIdentityEvidenceV0 {
131    let selector_usage = summarize_selector_usage_evaluator_candidates_input(input);
132    let selector_count = selector_usage.results.len();
133    let mut reference_site_count = 0usize;
134    let mut direct_reference_site_count = 0usize;
135    let mut expanded_reference_site_count = 0usize;
136    let mut style_dependency_reference_site_count = 0usize;
137    let mut editable_direct_site_count = 0usize;
138    let mut reference_kind_counts = BTreeMap::new();
139
140    for result in selector_usage.results {
141        editable_direct_site_count += result.payload.editable_direct_sites.len();
142        for site in result.payload.all_sites {
143            reference_site_count += 1;
144            if site.expansion == "direct" {
145                direct_reference_site_count += 1;
146            } else {
147                expanded_reference_site_count += 1;
148            }
149            if site.reference_kind == "styleDependency" {
150                style_dependency_reference_site_count += 1;
151            }
152            *reference_kind_counts
153                .entry(site.reference_kind)
154                .or_insert(0) += 1;
155        }
156    }
157
158    ReferenceSiteIdentityEvidenceV0 {
159        status: if reference_site_count > 0 {
160            "ready"
161        } else {
162            "gap"
163        },
164        selector_count,
165        reference_site_count,
166        direct_reference_site_count,
167        expanded_reference_site_count,
168        style_dependency_reference_site_count,
169        editable_direct_site_count,
170        reference_kind_counts,
171    }
172}
173
174fn summarize_certainty_reason(input: &EngineInputV2) -> CertaintyReasonEvidenceV0 {
175    let expression_semantics = summarize_expression_semantics_evaluator_candidates_input(input);
176    let mut expression_count = 0usize;
177    let mut exact_count = 0usize;
178    let mut inferred_count = 0usize;
179    let mut possible_count = 0usize;
180    let mut missing_reason_count = 0usize;
181    let mut reason_counts = BTreeMap::new();
182    let mut shape_kind_counts = BTreeMap::new();
183    let mut shape_label_counts = BTreeMap::new();
184
185    for result in expression_semantics.results {
186        expression_count += 1;
187        let payload = result.payload;
188        match payload.selector_certainty.as_str() {
189            "exact" => exact_count += 1,
190            "inferred" => inferred_count += 1,
191            "possible" => possible_count += 1,
192            _ => {}
193        }
194        *shape_kind_counts
195            .entry(payload.selector_certainty_shape_kind.clone())
196            .or_insert(0) += 1;
197        *shape_label_counts
198            .entry(payload.selector_certainty_shape_label.clone())
199            .or_insert(0) += 1;
200
201        if let Some(reason) = selector_certainty_reason(&payload) {
202            *reason_counts.entry(reason).or_insert(0) += 1;
203        } else {
204            missing_reason_count += 1;
205        }
206    }
207
208    CertaintyReasonEvidenceV0 {
209        status: if expression_count == 0 {
210            "gap"
211        } else if missing_reason_count == 0 {
212            "ready"
213        } else {
214            "partial"
215        },
216        expression_count,
217        exact_count,
218        inferred_count,
219        possible_count,
220        missing_reason_count,
221        reason_counts,
222        shape_kind_counts,
223        shape_label_counts,
224    }
225}
226
227fn summarize_binding_origin(input: &EngineInputV2) -> BindingOriginEvidenceV0 {
228    let mut expression_count = 0usize;
229    let mut direct_class_name_count = 0usize;
230    let mut root_binding_count = 0usize;
231    let mut access_path_count = 0usize;
232    let mut access_path_segment_count = 0usize;
233    let mut expression_kind_counts = BTreeMap::new();
234
235    for source in &input.sources {
236        for expression in &source.document.class_expressions {
237            expression_count += 1;
238            *expression_kind_counts
239                .entry(expression.kind.clone())
240                .or_insert(0) += 1;
241            if expression.class_name.is_some() {
242                direct_class_name_count += 1;
243            }
244            if expression.root_binding_decl_id.is_some() {
245                root_binding_count += 1;
246            }
247            if let Some(access_path) = &expression.access_path {
248                access_path_count += 1;
249                access_path_segment_count += access_path.len();
250            }
251        }
252    }
253
254    BindingOriginEvidenceV0 {
255        status: if expression_count == 0 {
256            "gap"
257        } else if direct_class_name_count + root_binding_count + access_path_count > 0 {
258            "ready"
259        } else {
260            "partial"
261        },
262        expression_count,
263        direct_class_name_count,
264        root_binding_count,
265        access_path_count,
266        access_path_segment_count,
267        expression_kind_counts,
268    }
269}
270
271fn summarize_style_module_edge(input: &EngineInputV2) -> StyleModuleEdgeEvidenceV0 {
272    let style_paths = input
273        .styles
274        .iter()
275        .map(|style| style.file_path.clone())
276        .collect::<BTreeSet<_>>();
277    let mut referenced_style_paths = BTreeSet::new();
278    let mut source_style_edge_count = 0usize;
279    let mut missing_style_document_edge_count = 0usize;
280    let mut composed_edge_count = 0usize;
281    let mut imported_composed_edge_count = 0usize;
282    let mut global_composed_edge_count = 0usize;
283
284    for source in &input.sources {
285        for expression in &source.document.class_expressions {
286            source_style_edge_count += 1;
287            referenced_style_paths.insert(expression.scss_module_path.clone());
288            if !style_paths.contains(&expression.scss_module_path) {
289                missing_style_document_edge_count += 1;
290            }
291        }
292    }
293
294    for style in &input.styles {
295        for selector in &style.document.selectors {
296            let Some(composes) = &selector.composes else {
297                continue;
298            };
299            composed_edge_count += composes.len();
300            for compose in composes {
301                if compose
302                    .get("fromGlobal")
303                    .and_then(|value| value.as_bool())
304                    .unwrap_or(false)
305                {
306                    global_composed_edge_count += 1;
307                } else if compose
308                    .get("from")
309                    .and_then(|value| value.as_str())
310                    .is_some()
311                {
312                    imported_composed_edge_count += 1;
313                }
314            }
315        }
316    }
317
318    StyleModuleEdgeEvidenceV0 {
319        status: if source_style_edge_count == 0 {
320            "gap"
321        } else if missing_style_document_edge_count == 0 {
322            "ready"
323        } else {
324            "partial"
325        },
326        source_style_edge_count,
327        distinct_style_module_count: referenced_style_paths.len(),
328        missing_style_document_edge_count,
329        composed_edge_count,
330        imported_composed_edge_count,
331        global_composed_edge_count,
332    }
333}
334
335fn summarize_value_domain_explanation(input: &EngineInputV2) -> ValueDomainExplanationEvidenceV0 {
336    let expression_semantics = summarize_expression_semantics_evaluator_candidates_input(input);
337    let mut expression_count = 0usize;
338    let mut exact_expression_count = 0usize;
339    let mut finite_value_expression_count = 0usize;
340    let mut constrained_expression_count = 0usize;
341    let mut unknown_expression_count = 0usize;
342    let mut finite_value_count = 0usize;
343    let mut value_domain_kind_counts = BTreeMap::new();
344    let mut constraint_kind_counts = BTreeMap::new();
345
346    for result in expression_semantics.results {
347        expression_count += 1;
348        let payload = result.payload;
349        *value_domain_kind_counts
350            .entry(payload.value_domain_kind.clone())
351            .or_insert(0) += 1;
352
353        match payload.value_domain_kind.as_str() {
354            "exact" => exact_expression_count += 1,
355            "finiteSet" => finite_value_expression_count += 1,
356            "constrained" => constrained_expression_count += 1,
357            "none" | "unknown" | "top" => unknown_expression_count += 1,
358            _ => {}
359        }
360
361        if let Some(values) = &payload.finite_values {
362            finite_value_count += values.len();
363        }
364        if let Some(kind) = &payload.value_constraint_kind {
365            *constraint_kind_counts.entry(kind.clone()).or_insert(0) += 1;
366        }
367    }
368
369    ValueDomainExplanationEvidenceV0 {
370        status: if expression_count == 0 {
371            "gap"
372        } else if exact_expression_count
373            + finite_value_expression_count
374            + constrained_expression_count
375            > 0
376        {
377            "ready"
378        } else {
379            "partial"
380        },
381        expression_count,
382        exact_expression_count,
383        finite_value_expression_count,
384        constrained_expression_count,
385        unknown_expression_count,
386        finite_value_count,
387        value_domain_kind_counts,
388        constraint_kind_counts,
389    }
390}
391
392fn selector_certainty_reason(
393    payload: &ExpressionSemanticsEvaluatorCandidatePayloadV0,
394) -> Option<String> {
395    match payload.selector_certainty.as_str() {
396        "exact" => {
397            if payload.selector_names.len() == 1 {
398                Some("single selector matched".to_string())
399            } else {
400                Some("selector set exactly matched the proven value domain".to_string())
401            }
402        }
403        "inferred" => match payload.selector_constraint_kind.as_deref() {
404            Some("prefix" | "suffix" | "prefixSuffix" | "charInclusion" | "composite") => {
405                Some("constrained runtime shape matched a bounded selector set".to_string())
406            }
407            _ => Some("finite candidate values matched a bounded selector set".to_string()),
408        },
409        "possible" => {
410            if payload.selector_names.is_empty() {
411                Some("no selector could be proven for this value".to_string())
412            } else {
413                Some("analysis could not prove an exact selector set".to_string())
414            }
415        }
416        _ => None,
417    }
418}