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 derivation_count: usize,
87    pub derivation_step_count: usize,
88    pub value_domain_kind_counts: BTreeMap<String, usize>,
89    pub constraint_kind_counts: BTreeMap<String, usize>,
90    pub derivation_product_counts: BTreeMap<String, usize>,
91    pub derivation_reduced_kind_counts: BTreeMap<String, usize>,
92    pub derivation_operation_counts: BTreeMap<String, usize>,
93}
94
95pub fn summarize_omena_bridge_source_input_evidence(
96    input: &EngineInputV2,
97) -> SourceInputPromotionEvidenceSummaryV0 {
98    let reference_site_identity = summarize_reference_site_identity(input);
99    let certainty_reason = summarize_certainty_reason(input);
100    let binding_origin = summarize_binding_origin(input);
101    let style_module_edge = summarize_style_module_edge(input);
102    let value_domain_explanation = summarize_value_domain_explanation(input);
103    let mut blocking_gaps = Vec::new();
104
105    if reference_site_identity.status == "gap" {
106        blocking_gaps.push("referenceSiteIdentity");
107    }
108    if certainty_reason.status == "gap" {
109        blocking_gaps.push("certaintyReason");
110    }
111    if binding_origin.status == "gap" {
112        blocking_gaps.push("bindingOrigin");
113    }
114    if style_module_edge.status == "gap" {
115        blocking_gaps.push("styleModuleEdge");
116    }
117    if value_domain_explanation.status == "gap" {
118        blocking_gaps.push("valueDomainExplanation");
119    }
120
121    SourceInputPromotionEvidenceSummaryV0 {
122        schema_version: "0",
123        product: "omena-semantic.source-input-evidence",
124        input_version: input.version.clone(),
125        reference_site_identity,
126        certainty_reason,
127        binding_origin,
128        style_module_edge,
129        value_domain_explanation,
130        blocking_gaps,
131        next_priorities: Vec::new(),
132    }
133}
134
135fn summarize_reference_site_identity(input: &EngineInputV2) -> ReferenceSiteIdentityEvidenceV0 {
136    let selector_usage = summarize_selector_usage_evaluator_candidates_input(input);
137    let selector_count = selector_usage.results.len();
138    let mut reference_site_count = 0usize;
139    let mut direct_reference_site_count = 0usize;
140    let mut expanded_reference_site_count = 0usize;
141    let mut style_dependency_reference_site_count = 0usize;
142    let mut editable_direct_site_count = 0usize;
143    let mut reference_kind_counts = BTreeMap::new();
144
145    for result in selector_usage.results {
146        editable_direct_site_count += result.payload.editable_direct_sites.len();
147        for site in result.payload.all_sites {
148            reference_site_count += 1;
149            if site.expansion == "direct" {
150                direct_reference_site_count += 1;
151            } else {
152                expanded_reference_site_count += 1;
153            }
154            if site.reference_kind == "styleDependency" {
155                style_dependency_reference_site_count += 1;
156            }
157            *reference_kind_counts
158                .entry(site.reference_kind)
159                .or_insert(0) += 1;
160        }
161    }
162
163    ReferenceSiteIdentityEvidenceV0 {
164        status: if reference_site_count > 0 {
165            "ready"
166        } else {
167            "gap"
168        },
169        selector_count,
170        reference_site_count,
171        direct_reference_site_count,
172        expanded_reference_site_count,
173        style_dependency_reference_site_count,
174        editable_direct_site_count,
175        reference_kind_counts,
176    }
177}
178
179fn summarize_certainty_reason(input: &EngineInputV2) -> CertaintyReasonEvidenceV0 {
180    let expression_semantics = summarize_expression_semantics_evaluator_candidates_input(input);
181    let mut expression_count = 0usize;
182    let mut exact_count = 0usize;
183    let mut inferred_count = 0usize;
184    let mut possible_count = 0usize;
185    let mut missing_reason_count = 0usize;
186    let mut reason_counts = BTreeMap::new();
187    let mut shape_kind_counts = BTreeMap::new();
188    let mut shape_label_counts = BTreeMap::new();
189
190    for result in expression_semantics.results {
191        expression_count += 1;
192        let payload = result.payload;
193        match payload.selector_certainty.as_str() {
194            "exact" => exact_count += 1,
195            "inferred" => inferred_count += 1,
196            "possible" => possible_count += 1,
197            _ => {}
198        }
199        *shape_kind_counts
200            .entry(payload.selector_certainty_shape_kind.clone())
201            .or_insert(0) += 1;
202        *shape_label_counts
203            .entry(payload.selector_certainty_shape_label.clone())
204            .or_insert(0) += 1;
205
206        if let Some(reason) = selector_certainty_reason(&payload) {
207            *reason_counts.entry(reason).or_insert(0) += 1;
208        } else {
209            missing_reason_count += 1;
210        }
211    }
212
213    CertaintyReasonEvidenceV0 {
214        status: if expression_count == 0 {
215            "gap"
216        } else if missing_reason_count == 0 {
217            "ready"
218        } else {
219            "partial"
220        },
221        expression_count,
222        exact_count,
223        inferred_count,
224        possible_count,
225        missing_reason_count,
226        reason_counts,
227        shape_kind_counts,
228        shape_label_counts,
229    }
230}
231
232fn summarize_binding_origin(input: &EngineInputV2) -> BindingOriginEvidenceV0 {
233    let mut expression_count = 0usize;
234    let mut direct_class_name_count = 0usize;
235    let mut root_binding_count = 0usize;
236    let mut access_path_count = 0usize;
237    let mut access_path_segment_count = 0usize;
238    let mut expression_kind_counts = BTreeMap::new();
239
240    for source in &input.sources {
241        for expression in &source.document.class_expressions {
242            expression_count += 1;
243            *expression_kind_counts
244                .entry(expression.kind.clone())
245                .or_insert(0) += 1;
246            if expression.class_name.is_some() {
247                direct_class_name_count += 1;
248            }
249            if expression.root_binding_decl_id.is_some() {
250                root_binding_count += 1;
251            }
252            if let Some(access_path) = &expression.access_path {
253                access_path_count += 1;
254                access_path_segment_count += access_path.len();
255            }
256        }
257    }
258
259    BindingOriginEvidenceV0 {
260        status: if expression_count == 0 {
261            "gap"
262        } else if direct_class_name_count + root_binding_count + access_path_count > 0 {
263            "ready"
264        } else {
265            "partial"
266        },
267        expression_count,
268        direct_class_name_count,
269        root_binding_count,
270        access_path_count,
271        access_path_segment_count,
272        expression_kind_counts,
273    }
274}
275
276fn summarize_style_module_edge(input: &EngineInputV2) -> StyleModuleEdgeEvidenceV0 {
277    let style_paths = input
278        .styles
279        .iter()
280        .map(|style| style.file_path.clone())
281        .collect::<BTreeSet<_>>();
282    let mut referenced_style_paths = BTreeSet::new();
283    let mut source_style_edge_count = 0usize;
284    let mut missing_style_document_edge_count = 0usize;
285    let mut composed_edge_count = 0usize;
286    let mut imported_composed_edge_count = 0usize;
287    let mut global_composed_edge_count = 0usize;
288
289    for source in &input.sources {
290        for expression in &source.document.class_expressions {
291            source_style_edge_count += 1;
292            referenced_style_paths.insert(expression.scss_module_path.clone());
293            if !style_paths.contains(&expression.scss_module_path) {
294                missing_style_document_edge_count += 1;
295            }
296        }
297    }
298
299    for style in &input.styles {
300        for selector in &style.document.selectors {
301            let Some(composes) = &selector.composes else {
302                continue;
303            };
304            composed_edge_count += composes.len();
305            for compose in composes {
306                if compose
307                    .get("fromGlobal")
308                    .and_then(|value| value.as_bool())
309                    .unwrap_or(false)
310                {
311                    global_composed_edge_count += 1;
312                } else if compose
313                    .get("from")
314                    .and_then(|value| value.as_str())
315                    .is_some()
316                {
317                    imported_composed_edge_count += 1;
318                }
319            }
320        }
321    }
322
323    StyleModuleEdgeEvidenceV0 {
324        status: if source_style_edge_count == 0 {
325            "gap"
326        } else if missing_style_document_edge_count == 0 {
327            "ready"
328        } else {
329            "partial"
330        },
331        source_style_edge_count,
332        distinct_style_module_count: referenced_style_paths.len(),
333        missing_style_document_edge_count,
334        composed_edge_count,
335        imported_composed_edge_count,
336        global_composed_edge_count,
337    }
338}
339
340fn summarize_value_domain_explanation(input: &EngineInputV2) -> ValueDomainExplanationEvidenceV0 {
341    let expression_semantics = summarize_expression_semantics_evaluator_candidates_input(input);
342    let mut expression_count = 0usize;
343    let mut exact_expression_count = 0usize;
344    let mut finite_value_expression_count = 0usize;
345    let mut constrained_expression_count = 0usize;
346    let mut unknown_expression_count = 0usize;
347    let mut finite_value_count = 0usize;
348    let mut derivation_count = 0usize;
349    let mut derivation_step_count = 0usize;
350    let mut value_domain_kind_counts = BTreeMap::new();
351    let mut constraint_kind_counts = BTreeMap::new();
352    let mut derivation_product_counts = BTreeMap::new();
353    let mut derivation_reduced_kind_counts = BTreeMap::new();
354    let mut derivation_operation_counts = BTreeMap::new();
355
356    for result in expression_semantics.results {
357        expression_count += 1;
358        let payload = result.payload;
359        *value_domain_kind_counts
360            .entry(payload.value_domain_kind.clone())
361            .or_insert(0) += 1;
362
363        match payload.value_domain_kind.as_str() {
364            "exact" => exact_expression_count += 1,
365            "finiteSet" => finite_value_expression_count += 1,
366            "constrained" => constrained_expression_count += 1,
367            "none" | "unknown" | "top" => unknown_expression_count += 1,
368            _ => {}
369        }
370
371        if let Some(values) = &payload.finite_values {
372            finite_value_count += values.len();
373        }
374        if let Some(kind) = &payload.value_constraint_kind {
375            *constraint_kind_counts.entry(kind.clone()).or_insert(0) += 1;
376        }
377
378        derivation_count += 1;
379        let derivation = payload.value_domain_derivation;
380        *derivation_product_counts
381            .entry(derivation.product.to_string())
382            .or_insert(0) += 1;
383        *derivation_reduced_kind_counts
384            .entry(derivation.reduced_kind.to_string())
385            .or_insert(0) += 1;
386        for step in derivation.steps {
387            derivation_step_count += 1;
388            *derivation_operation_counts
389                .entry(step.operation.to_string())
390                .or_insert(0) += 1;
391        }
392    }
393
394    ValueDomainExplanationEvidenceV0 {
395        status: if expression_count == 0 {
396            "gap"
397        } else if exact_expression_count
398            + finite_value_expression_count
399            + constrained_expression_count
400            > 0
401            && derivation_count == expression_count
402        {
403            "ready"
404        } else {
405            "partial"
406        },
407        expression_count,
408        exact_expression_count,
409        finite_value_expression_count,
410        constrained_expression_count,
411        unknown_expression_count,
412        finite_value_count,
413        derivation_count,
414        derivation_step_count,
415        value_domain_kind_counts,
416        constraint_kind_counts,
417        derivation_product_counts,
418        derivation_reduced_kind_counts,
419        derivation_operation_counts,
420    }
421}
422
423fn selector_certainty_reason(
424    payload: &ExpressionSemanticsEvaluatorCandidatePayloadV0,
425) -> Option<String> {
426    match payload.selector_certainty.as_str() {
427        "exact" => {
428            if payload.selector_names.len() == 1 {
429                Some("single selector matched".to_string())
430            } else {
431                Some("selector set exactly matched the proven value domain".to_string())
432            }
433        }
434        "inferred" => match payload.selector_constraint_kind.as_deref() {
435            Some("prefix" | "suffix" | "prefixSuffix" | "charInclusion" | "composite") => {
436                Some("constrained runtime shape matched a bounded selector set".to_string())
437            }
438            _ => Some("finite candidate values matched a bounded selector set".to_string()),
439        },
440        "possible" => {
441            if payload.selector_names.is_empty() {
442                Some("no selector could be proven for this value".to_string())
443            } else {
444                Some("analysis could not prove an exact selector set".to_string())
445            }
446        }
447        _ => None,
448    }
449}