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