1use std::collections::{BTreeMap, BTreeSet};
2use std::path::{Component, Path, PathBuf};
3
4use crate::{
5 EngineInputV2, RangeV2, SelectorUsageCandidateV0, SelectorUsageCandidatesV0,
6 SelectorUsageCanonicalCandidateBundleV0, SelectorUsageCanonicalProducerSignalV0,
7 SelectorUsageEditableDirectSiteV0, SelectorUsageEvaluatorCandidatePayloadV0,
8 SelectorUsageEvaluatorCandidateV0, SelectorUsageEvaluatorCandidatesV0, SelectorUsageFragmentV0,
9 SelectorUsageFragmentsV0, SelectorUsagePlanSummaryV0, SelectorUsageQueryFragmentV0,
10 SelectorUsageQueryFragmentsV0, SelectorUsageReferenceSiteV0, StyleAnalysisInputV2,
11 StyleSelectorV2, canonical_selector_count,
12 expression_domain::{
13 ExpressionDomainSelectorCertaintyFlowHedgesV0,
14 collect_expression_domain_selector_certainty_flow_hedges,
15 },
16 map_selector_certainty_projection, resolve_selector_names,
17};
18
19pub fn summarize_selector_usage_plan_input(input: &EngineInputV2) -> SelectorUsagePlanSummaryV0 {
20 let mut canonical_selector_names = Vec::new();
21 let mut view_kind_counts = BTreeMap::new();
22 let mut nested_safety_counts = BTreeMap::new();
23 let mut composed_selector_count = 0usize;
24 let mut total_composes_refs = 0usize;
25
26 for style in &input.styles {
27 for selector in &style.document.selectors {
28 *view_kind_counts
29 .entry(selector.view_kind.clone())
30 .or_insert(0) += 1;
31
32 if let Some(nested_safety) = &selector.nested_safety {
33 *nested_safety_counts
34 .entry(nested_safety.clone())
35 .or_insert(0) += 1;
36 }
37
38 let composes_len = selector.composes.as_ref().map_or(0, Vec::len);
39 if composes_len > 0 {
40 composed_selector_count += 1;
41 total_composes_refs += composes_len;
42 }
43
44 if selector.view_kind == "canonical"
45 && let Some(canonical_name) = &selector.canonical_name
46 {
47 canonical_selector_names.push(canonical_name.clone());
48 }
49 }
50 }
51
52 SelectorUsagePlanSummaryV0 {
53 schema_version: "0",
54 input_version: input.version.clone(),
55 canonical_selector_names,
56 view_kind_counts,
57 nested_safety_counts,
58 composed_selector_count,
59 total_composes_refs,
60 }
61}
62
63pub fn summarize_selector_usage_fragments_input(input: &EngineInputV2) -> SelectorUsageFragmentsV0 {
64 let mut fragments = Vec::new();
65
66 for style in &input.styles {
67 for (ordinal, selector) in style.document.selectors.iter().enumerate() {
68 fragments.push(SelectorUsageFragmentV0 {
69 ordinal,
70 view_kind: selector.view_kind.clone(),
71 canonical_name: selector.canonical_name.clone(),
72 nested_safety: selector.nested_safety.clone(),
73 composes_count: selector.composes.as_ref().map_or(0, Vec::len),
74 });
75 }
76 }
77
78 SelectorUsageFragmentsV0 {
79 schema_version: "0",
80 input_version: input.version.clone(),
81 fragments,
82 }
83}
84
85pub fn summarize_selector_usage_query_fragments_input(
86 input: &EngineInputV2,
87) -> SelectorUsageQueryFragmentsV0 {
88 let mut fragments = Vec::new();
89
90 for style in &input.styles {
91 for selector in &style.document.selectors {
92 if selector.view_kind != "canonical" {
93 continue;
94 }
95 let Some(canonical_name) = &selector.canonical_name else {
96 continue;
97 };
98 fragments.push(SelectorUsageQueryFragmentV0 {
99 query_id: canonical_name.clone(),
100 canonical_name: canonical_name.clone(),
101 nested_safety: selector.nested_safety.clone(),
102 composes_count: selector.composes.as_ref().map_or(0, Vec::len),
103 });
104 }
105 }
106
107 fragments.sort_by(|a, b| a.query_id.cmp(&b.query_id));
108
109 SelectorUsageQueryFragmentsV0 {
110 schema_version: "0",
111 input_version: input.version.clone(),
112 fragments,
113 }
114}
115
116#[derive(Default, Clone)]
117struct SelectorUsageAggregate {
118 total_references: usize,
119 direct_reference_count: usize,
120 editable_direct_reference_count: usize,
121 exact_reference_count: usize,
122 inferred_or_better_reference_count: usize,
123 has_expanded_references: bool,
124 all_sites: Vec<SelectorUsageReferenceSiteV0>,
125 editable_direct_sites: Vec<SelectorUsageEditableDirectSiteV0>,
126}
127
128struct SelectorUsageInputRows {
129 query_fragments: Vec<SelectorUsageQueryFragmentV0>,
130 fragments: Vec<SelectorUsageFragmentV0>,
131 candidates: Vec<SelectorUsageCandidateV0>,
132 evaluator_candidates: Vec<SelectorUsageEvaluatorCandidateV0>,
133}
134
135pub fn summarize_selector_usage_candidates_input(
136 input: &EngineInputV2,
137) -> SelectorUsageCandidatesV0 {
138 let rows = collect_selector_usage_input_rows(input);
139
140 SelectorUsageCandidatesV0 {
141 schema_version: "0",
142 input_version: input.version.clone(),
143 candidates: rows.candidates,
144 }
145}
146
147pub fn summarize_selector_usage_evaluator_candidates_input(
148 input: &EngineInputV2,
149) -> SelectorUsageEvaluatorCandidatesV0 {
150 let rows = collect_selector_usage_input_rows(input);
151
152 SelectorUsageEvaluatorCandidatesV0 {
153 schema_version: "0",
154 input_version: input.version.clone(),
155 results: rows.evaluator_candidates,
156 }
157}
158
159pub fn summarize_selector_usage_canonical_candidate_bundle_input(
160 input: &EngineInputV2,
161) -> SelectorUsageCanonicalCandidateBundleV0 {
162 let rows = collect_selector_usage_input_rows(input);
163
164 SelectorUsageCanonicalCandidateBundleV0 {
165 schema_version: "0",
166 input_version: input.version.clone(),
167 query_fragments: rows.query_fragments,
168 fragments: rows.fragments,
169 candidates: rows.candidates,
170 }
171}
172
173pub fn summarize_selector_usage_canonical_producer_signal_input(
174 input: &EngineInputV2,
175) -> SelectorUsageCanonicalProducerSignalV0 {
176 let rows = collect_selector_usage_input_rows(input);
177 let input_version = input.version.clone();
178
179 SelectorUsageCanonicalProducerSignalV0 {
180 schema_version: "0",
181 input_version: input_version.clone(),
182 canonical_bundle: SelectorUsageCanonicalCandidateBundleV0 {
183 schema_version: "0",
184 input_version: input_version.clone(),
185 query_fragments: rows.query_fragments.clone(),
186 fragments: rows.fragments.clone(),
187 candidates: rows.candidates.clone(),
188 },
189 evaluator_candidates: SelectorUsageEvaluatorCandidatesV0 {
190 schema_version: "0",
191 input_version,
192 results: rows.evaluator_candidates,
193 },
194 }
195}
196
197fn collect_selector_usage_input_rows(input: &EngineInputV2) -> SelectorUsageInputRows {
198 let selector_certainty_flow_hedges =
199 collect_expression_domain_selector_certainty_flow_hedges(input);
200 collect_selector_usage_input_rows_with_flow_hedges(input, &selector_certainty_flow_hedges)
201}
202
203fn collect_selector_usage_input_rows_with_flow_hedges(
204 input: &EngineInputV2,
205 selector_certainty_flow_hedges: &ExpressionDomainSelectorCertaintyFlowHedgesV0,
206) -> SelectorUsageInputRows {
207 let mut expression_index = BTreeMap::new();
208 let mut style_index = BTreeMap::new();
209 let mut canonical_by_file = BTreeMap::<String, BTreeSet<String>>::new();
210
211 for source in &input.sources {
212 for expression in &source.document.class_expressions {
213 expression_index.insert(expression.id.clone(), expression);
214 }
215 }
216
217 for style in &input.styles {
218 style_index.insert(style.file_path.clone(), style);
219 let names = canonical_by_file
220 .entry(style.file_path.clone())
221 .or_default();
222 for selector in &style.document.selectors {
223 if selector.view_kind != "canonical" {
224 continue;
225 }
226 if let Some(canonical_name) = &selector.canonical_name {
227 names.insert(canonical_name.clone());
228 }
229 }
230 }
231
232 let mut source_counts = BTreeMap::<(String, String), SelectorUsageAggregate>::new();
233
234 for entry in &input.type_facts {
235 let Some(expression) = expression_index.get(&entry.expression_id) else {
236 continue;
237 };
238 let Some(style) = style_index.get(&expression.scss_module_path) else {
239 continue;
240 };
241
242 let selector_names = resolve_selector_names(style, &entry.facts);
243 if selector_names.is_empty() {
244 continue;
245 }
246
247 let selector_certainty = map_selector_certainty_projection(
248 &entry.facts,
249 selector_names.len(),
250 canonical_selector_count(style),
251 selector_certainty_flow_hedges
252 .get(&(entry.file_path.clone(), entry.expression_id.clone())),
253 )
254 .certainty;
255 let is_direct_source = matches!(expression.kind.as_str(), "literal" | "styleAccess");
256
257 for selector_name in selector_names {
258 let counts = source_counts
259 .entry((expression.scss_module_path.clone(), selector_name))
260 .or_default();
261 counts.total_references += 1;
262 push_usage_site(
263 &mut counts.all_sites,
264 SelectorUsageReferenceSiteV0 {
265 file_path: entry.file_path.clone(),
266 range: expression.range.clone(),
267 expansion: if is_direct_source {
268 "direct".to_string()
269 } else {
270 "expanded".to_string()
271 },
272 reference_kind: "source".to_string(),
273 },
274 );
275 if is_direct_source {
276 counts.direct_reference_count += 1;
277 counts.editable_direct_reference_count += 1;
278 if let Some(class_name) = &expression.class_name {
279 push_usage_editable_direct_site(
280 &mut counts.editable_direct_sites,
281 SelectorUsageEditableDirectSiteV0 {
282 file_path: entry.file_path.clone(),
283 range: expression.range.clone(),
284 class_name: class_name.clone(),
285 },
286 );
287 }
288 } else {
289 counts.has_expanded_references = true;
290 }
291 match selector_certainty.as_str() {
292 "exact" => {
293 counts.exact_reference_count += 1;
294 counts.inferred_or_better_reference_count += 1;
295 }
296 "inferred" => {
297 counts.inferred_or_better_reference_count += 1;
298 }
299 _ => {}
300 }
301 }
302 }
303
304 let incoming_style_dependencies = build_incoming_style_dependencies(input, &canonical_by_file);
305
306 let fragments = summarize_selector_usage_fragments_input(input).fragments;
307 let mut query_fragments = summarize_selector_usage_query_fragments_input(input).fragments;
308 query_fragments.sort_by(|a, b| a.query_id.cmp(&b.query_id));
309
310 let mut candidates = Vec::new();
311 let mut evaluator_candidates = Vec::new();
312
313 for style in &input.styles {
314 for selector in &style.document.selectors {
315 if selector.view_kind != "canonical" {
316 continue;
317 }
318 let Some(canonical_name) = &selector.canonical_name else {
319 continue;
320 };
321
322 let mut counts = source_counts
323 .remove(&(style.file_path.clone(), canonical_name.clone()))
324 .unwrap_or_default();
325 let style_dependency_sites = collect_incoming_style_dependency_sites(
326 &incoming_style_dependencies,
327 &style_index,
328 &style.file_path,
329 canonical_name,
330 );
331 let style_dependency_count = style_dependency_sites.len();
332 for site in style_dependency_sites {
333 push_usage_site(&mut counts.all_sites, site);
334 }
335
336 counts.total_references += style_dependency_count;
337 counts.direct_reference_count += style_dependency_count;
338 counts.exact_reference_count += style_dependency_count;
339 counts.inferred_or_better_reference_count += style_dependency_count;
340
341 let has_style_dependency_references = style_dependency_count > 0;
342 let has_any_references = counts.total_references > 0;
343
344 let candidate = SelectorUsageCandidateV0 {
345 query_id: canonical_name.clone(),
346 canonical_name: canonical_name.clone(),
347 file_path: style.file_path.clone(),
348 total_references: counts.total_references,
349 direct_reference_count: counts.direct_reference_count,
350 editable_direct_reference_count: counts.editable_direct_reference_count,
351 exact_reference_count: counts.exact_reference_count,
352 inferred_or_better_reference_count: counts.inferred_or_better_reference_count,
353 has_expanded_references: counts.has_expanded_references,
354 has_style_dependency_references,
355 has_any_references,
356 };
357
358 candidates.push(candidate.clone());
359 evaluator_candidates.push(SelectorUsageEvaluatorCandidateV0 {
360 kind: "selector-usage",
361 file_path: style.file_path.clone(),
362 query_id: canonical_name.clone(),
363 payload: SelectorUsageEvaluatorCandidatePayloadV0 {
364 canonical_name: canonical_name.clone(),
365 total_references: candidate.total_references,
366 direct_reference_count: candidate.direct_reference_count,
367 editable_direct_reference_count: candidate.editable_direct_reference_count,
368 exact_reference_count: candidate.exact_reference_count,
369 inferred_or_better_reference_count: candidate
370 .inferred_or_better_reference_count,
371 has_expanded_references: candidate.has_expanded_references,
372 has_style_dependency_references: candidate.has_style_dependency_references,
373 has_any_references: candidate.has_any_references,
374 all_sites: counts.all_sites.clone(),
375 editable_direct_sites: counts.editable_direct_sites.clone(),
376 },
377 });
378 }
379 }
380
381 candidates.sort_by(|a, b| {
382 a.file_path
383 .cmp(&b.file_path)
384 .then(a.query_id.cmp(&b.query_id))
385 });
386 evaluator_candidates.sort_by(|a, b| {
387 a.file_path
388 .cmp(&b.file_path)
389 .then(a.query_id.cmp(&b.query_id))
390 });
391
392 SelectorUsageInputRows {
393 query_fragments,
394 fragments,
395 candidates,
396 evaluator_candidates,
397 }
398}
399
400fn build_incoming_style_dependencies(
401 input: &EngineInputV2,
402 canonical_by_file: &BTreeMap<String, BTreeSet<String>>,
403) -> BTreeMap<(String, String), BTreeSet<(String, String)>> {
404 let mut incoming = BTreeMap::<(String, String), BTreeSet<(String, String)>>::new();
405
406 for style in &input.styles {
407 for selector in &style.document.selectors {
408 if selector.view_kind != "canonical" {
409 continue;
410 }
411 let Some(incoming_canonical_name) = &selector.canonical_name else {
412 continue;
413 };
414 let Some(composes) = &selector.composes else {
415 continue;
416 };
417
418 for compose in composes {
419 let Some(class_names) = compose
420 .get("classNames")
421 .and_then(|value| value.as_array())
422 .map(|values| {
423 values
424 .iter()
425 .filter_map(|value| value.as_str().map(ToString::to_string))
426 .collect::<Vec<_>>()
427 })
428 else {
429 continue;
430 };
431 if class_names.is_empty() {
432 continue;
433 }
434 if compose
435 .get("fromGlobal")
436 .and_then(|value| value.as_bool())
437 .unwrap_or(false)
438 {
439 continue;
440 }
441
442 let target_file = compose
443 .get("from")
444 .and_then(|value| value.as_str())
445 .map(|from| normalize_joined_path(&style.file_path, from))
446 .unwrap_or_else(|| style.file_path.clone());
447
448 let Some(target_names) = canonical_by_file.get(&target_file) else {
449 continue;
450 };
451
452 for class_name in class_names {
453 if !target_names.contains(&class_name) {
454 continue;
455 }
456 incoming
457 .entry((target_file.clone(), class_name))
458 .or_default()
459 .insert((style.file_path.clone(), incoming_canonical_name.clone()));
460 }
461 }
462 }
463 }
464
465 incoming
466}
467
468fn collect_incoming_style_dependency_sites(
469 incoming: &BTreeMap<(String, String), BTreeSet<(String, String)>>,
470 style_index: &BTreeMap<String, &StyleAnalysisInputV2>,
471 file_path: &str,
472 canonical_name: &str,
473) -> Vec<SelectorUsageReferenceSiteV0> {
474 let mut seen = BTreeSet::<(String, String)>::new();
475 let mut sites = Vec::<SelectorUsageReferenceSiteV0>::new();
476 collect_incoming_style_dependencies(
477 incoming,
478 style_index,
479 &(file_path.to_string(), canonical_name.to_string()),
480 &mut seen,
481 &mut sites,
482 );
483 sites.sort_by(|a, b| {
484 a.file_path
485 .cmp(&b.file_path)
486 .then(a.range.start.line.cmp(&b.range.start.line))
487 .then(a.range.start.character.cmp(&b.range.start.character))
488 .then(a.range.end.line.cmp(&b.range.end.line))
489 .then(a.range.end.character.cmp(&b.range.end.character))
490 .then(a.reference_kind.cmp(&b.reference_kind))
491 .then(a.expansion.cmp(&b.expansion))
492 });
493 sites.dedup();
494 sites
495}
496
497fn collect_incoming_style_dependencies(
498 incoming: &BTreeMap<(String, String), BTreeSet<(String, String)>>,
499 style_index: &BTreeMap<String, &StyleAnalysisInputV2>,
500 key: &(String, String),
501 seen: &mut BTreeSet<(String, String)>,
502 sites: &mut Vec<SelectorUsageReferenceSiteV0>,
503) {
504 let Some(entries) = incoming.get(key) else {
505 return;
506 };
507 for entry in entries {
508 if seen.insert(entry.clone()) {
509 if let Some(style) = style_index.get(&entry.0)
510 && let Some(selector) = find_canonical_selector(style, &entry.1)
511 {
512 sites.push(SelectorUsageReferenceSiteV0 {
513 file_path: entry.0.clone(),
514 range: selector_site_range(selector),
515 expansion: "direct".to_string(),
516 reference_kind: "styleDependency".to_string(),
517 });
518 }
519 collect_incoming_style_dependencies(incoming, style_index, entry, seen, sites);
520 }
521 }
522}
523
524fn push_usage_site(
525 sites: &mut Vec<SelectorUsageReferenceSiteV0>,
526 site: SelectorUsageReferenceSiteV0,
527) {
528 if !sites.iter().any(|existing| existing == &site) {
529 sites.push(site);
530 }
531}
532
533fn push_usage_editable_direct_site(
534 sites: &mut Vec<SelectorUsageEditableDirectSiteV0>,
535 site: SelectorUsageEditableDirectSiteV0,
536) {
537 if !sites.iter().any(|existing| existing == &site) {
538 sites.push(site);
539 }
540}
541
542fn find_canonical_selector<'a>(
543 style: &'a StyleAnalysisInputV2,
544 canonical_name: &str,
545) -> Option<&'a StyleSelectorV2> {
546 style.document.selectors.iter().find(|selector| {
547 selector.view_kind == "canonical"
548 && selector.canonical_name.as_deref() == Some(canonical_name)
549 })
550}
551
552fn selector_site_range(selector: &StyleSelectorV2) -> RangeV2 {
553 selector
554 .bem_suffix
555 .as_ref()
556 .map(|suffix| suffix.raw_token_range.clone())
557 .unwrap_or_else(|| selector.range.clone())
558}
559
560fn normalize_joined_path(base_file_path: &str, relative_from: &str) -> String {
561 let base_dir = Path::new(base_file_path)
562 .parent()
563 .map(Path::to_path_buf)
564 .unwrap_or_default();
565 let joined = base_dir.join(relative_from);
566 let mut normalized = PathBuf::new();
567
568 for component in joined.components() {
569 match component {
570 Component::CurDir => {}
571 Component::ParentDir => {
572 normalized.pop();
573 }
574 other => normalized.push(other.as_os_str()),
575 }
576 }
577
578 normalized.to_string_lossy().into_owned()
579}
580
581#[cfg(test)]
582mod tests {
583 use super::{
584 summarize_selector_usage_candidates_input,
585 summarize_selector_usage_canonical_candidate_bundle_input,
586 summarize_selector_usage_canonical_producer_signal_input,
587 summarize_selector_usage_evaluator_candidates_input,
588 summarize_selector_usage_fragments_input, summarize_selector_usage_plan_input,
589 summarize_selector_usage_query_fragments_input,
590 };
591 use crate::{configure_nonconvergent_selector_certainty_fixture, test_support::sample_input};
592 use serde_json::json;
593
594 #[test]
595 fn summarizes_selector_usage_universe() {
596 let summary = summarize_selector_usage_plan_input(&sample_input());
597
598 assert_eq!(
599 summary.canonical_selector_names,
600 vec!["btn-active".to_string(), "card-header".to_string()]
601 );
602 assert_eq!(summary.view_kind_counts.get("canonical"), Some(&2));
603 assert_eq!(summary.view_kind_counts.get("nested"), Some(&1));
604 assert_eq!(summary.nested_safety_counts.get("safe"), Some(&1));
605 assert_eq!(summary.nested_safety_counts.get("unsafe"), Some(&1));
606 assert_eq!(summary.nested_safety_counts.get("unknown"), Some(&1));
607 assert_eq!(summary.composed_selector_count, 2);
608 assert_eq!(summary.total_composes_refs, 3);
609 }
610
611 #[test]
612 fn summarizes_selector_usage_fragments() {
613 let summary = summarize_selector_usage_fragments_input(&sample_input());
614
615 assert_eq!(summary.fragments.len(), 3);
616 assert_eq!(summary.fragments[0].ordinal, 0);
617 assert_eq!(summary.fragments[0].view_kind, "canonical");
618 assert_eq!(
619 summary.fragments[0].canonical_name.as_deref(),
620 Some("btn-active")
621 );
622 assert_eq!(summary.fragments[0].nested_safety.as_deref(), Some("safe"));
623 assert_eq!(summary.fragments[0].composes_count, 1);
624
625 assert_eq!(summary.fragments[2].ordinal, 1);
626 assert_eq!(summary.fragments[2].view_kind, "nested");
627 assert_eq!(
628 summary.fragments[2].canonical_name.as_deref(),
629 Some("card-header")
630 );
631 assert_eq!(
632 summary.fragments[2].nested_safety.as_deref(),
633 Some("unknown")
634 );
635 assert_eq!(summary.fragments[2].composes_count, 2);
636 }
637
638 #[test]
639 fn summarizes_selector_usage_query_fragments() {
640 let summary = summarize_selector_usage_query_fragments_input(&sample_input());
641
642 assert_eq!(summary.fragments.len(), 2);
643 assert_eq!(summary.fragments[0].query_id, "btn-active");
644 assert_eq!(summary.fragments[0].canonical_name, "btn-active");
645 assert_eq!(summary.fragments[0].nested_safety.as_deref(), Some("safe"));
646 assert_eq!(summary.fragments[0].composes_count, 1);
647
648 assert_eq!(summary.fragments[1].query_id, "card-header");
649 assert_eq!(summary.fragments[1].canonical_name, "card-header");
650 assert_eq!(
651 summary.fragments[1].nested_safety.as_deref(),
652 Some("unsafe")
653 );
654 assert_eq!(summary.fragments[1].composes_count, 0);
655 }
656
657 #[test]
658 fn summarizes_selector_usage_candidates() {
659 let mut input = sample_input();
660 input.styles[0].document.selectors[0].composes = Some(vec![json!({
661 "classNames": ["card-header"],
662 "from": "./Card.module.scss"
663 })]);
664
665 let summary = summarize_selector_usage_candidates_input(&input);
666
667 assert_eq!(summary.candidates.len(), 2);
668 let app = &summary.candidates[0];
669 assert_eq!(app.file_path, "/tmp/App.module.scss");
670 assert_eq!(app.query_id, "btn-active");
671 assert_eq!(app.total_references, 1);
672 assert_eq!(app.direct_reference_count, 0);
673 assert_eq!(app.editable_direct_reference_count, 0);
674 assert_eq!(app.exact_reference_count, 0);
675 assert_eq!(app.inferred_or_better_reference_count, 1);
676 assert!(app.has_expanded_references);
677 assert!(!app.has_style_dependency_references);
678 assert!(app.has_any_references);
679
680 let card = &summary.candidates[1];
681 assert_eq!(card.file_path, "/tmp/Card.module.scss");
682 assert_eq!(card.query_id, "card-header");
683 assert_eq!(card.total_references, 2);
684 assert_eq!(card.direct_reference_count, 2);
685 assert_eq!(card.editable_direct_reference_count, 1);
686 assert_eq!(card.exact_reference_count, 1);
687 assert_eq!(card.inferred_or_better_reference_count, 2);
688 assert!(!card.has_expanded_references);
689 assert!(card.has_style_dependency_references);
690 assert!(card.has_any_references);
691 }
692
693 #[test]
694 fn nonconverged_flow_hedge_demotes_selector_usage_aggregates() {
695 let mut input = sample_input();
696 input.styles[0].document.selectors[0].name = "x".to_string();
697 input.styles[0].document.selectors[0].canonical_name = Some("x".to_string());
698 configure_nonconvergent_selector_certainty_fixture(&mut input.type_facts[0], "x");
699 let product = summarize_selector_usage_canonical_producer_signal_input(&input);
700 let candidate = &product.canonical_bundle.candidates[0];
701
702 assert_eq!(candidate.query_id, "x");
703 assert_eq!(candidate.total_references, 1);
704 assert_eq!(candidate.exact_reference_count, 0);
705 assert_eq!(candidate.inferred_or_better_reference_count, 0);
706 assert_eq!(
707 product
708 .evaluator_candidates
709 .results
710 .iter()
711 .find(|entry| entry.query_id == "x")
712 .map(|entry| entry.payload.inferred_or_better_reference_count),
713 Some(0)
714 );
715 }
716
717 #[test]
718 fn summarizes_selector_usage_evaluator_candidates() {
719 let summary = summarize_selector_usage_evaluator_candidates_input(&sample_input());
720
721 assert_eq!(summary.results.len(), 2);
722 assert_eq!(summary.results[0].kind, "selector-usage");
723 assert_eq!(summary.results[0].file_path, "/tmp/App.module.scss");
724 assert_eq!(summary.results[0].query_id, "btn-active");
725 assert_eq!(summary.results[0].payload.all_sites.len(), 1);
726 assert_eq!(
727 summary.results[0].payload.all_sites[0].file_path,
728 "/tmp/App.tsx"
729 );
730 assert_eq!(
731 summary.results[0].payload.all_sites[0].expansion,
732 "expanded"
733 );
734 assert_eq!(
735 summary.results[0].payload.all_sites[0].reference_kind,
736 "source"
737 );
738 assert!(summary.results[0].payload.editable_direct_sites.is_empty());
739 assert_eq!(summary.results[1].payload.editable_direct_sites.len(), 1);
740 assert_eq!(
741 summary.results[1].payload.editable_direct_sites[0].file_path,
742 "/tmp/Card.tsx"
743 );
744 assert_eq!(
745 summary.results[1].payload.editable_direct_sites[0].class_name,
746 "card-header"
747 );
748 }
749
750 #[test]
751 fn summarizes_selector_usage_canonical_candidate_bundle() {
752 let summary = summarize_selector_usage_canonical_candidate_bundle_input(&sample_input());
753
754 assert_eq!(summary.query_fragments.len(), 2);
755 assert_eq!(summary.fragments.len(), 3);
756 assert_eq!(summary.candidates.len(), 2);
757 }
758
759 #[test]
760 fn summarizes_selector_usage_canonical_producer_signal() {
761 let summary = summarize_selector_usage_canonical_producer_signal_input(&sample_input());
762
763 assert_eq!(summary.canonical_bundle.candidates.len(), 2);
764 assert_eq!(summary.evaluator_candidates.results.len(), 2);
765 assert_eq!(
766 summary.evaluator_candidates.results[0].query_id,
767 "btn-active"
768 );
769 }
770}