Skip to main content

omena_semantic/
source_evidence.rs

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