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}