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}