1use 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}