Skip to main content

engine_input_producers/
source_resolution.rs

1use std::collections::{BTreeMap, BTreeSet};
2
3use crate::{
4    EngineInputV2, SourceResolutionCandidateV0, SourceResolutionCandidatesV0,
5    SourceResolutionCanonicalCandidateBundleV0, SourceResolutionCanonicalProducerSignalV0,
6    SourceResolutionEvaluatorCandidatePayloadV0, SourceResolutionEvaluatorCandidateV0,
7    SourceResolutionEvaluatorCandidatesV0, SourceResolutionFragmentV0, SourceResolutionFragmentsV0,
8    SourceResolutionMatchFragmentV0, SourceResolutionMatchFragmentsV0,
9    SourceResolutionPlanSummaryV0, SourceResolutionQueryFragmentV0,
10    SourceResolutionQueryFragmentsV0, canonical_selector_count,
11    expression_domain::{
12        ExpressionDomainSelectorCertaintyFlowHedgesV0,
13        collect_expression_domain_selector_certainty_flow_hedges,
14    },
15    finite_values_for_facts, map_selector_certainty_projection, map_value_certainty,
16    map_value_certainty_shape_kind, map_value_certainty_shape_label, resolve_selector_names,
17};
18
19struct SourceResolutionInputRows {
20    query_fragments: Vec<SourceResolutionQueryFragmentV0>,
21    fragments: Vec<SourceResolutionFragmentV0>,
22    match_fragments: Vec<SourceResolutionMatchFragmentV0>,
23    candidates: Vec<SourceResolutionCandidateV0>,
24    evaluator_candidates: Vec<SourceResolutionEvaluatorCandidateV0>,
25}
26
27fn collect_source_resolution_input_rows(input: &EngineInputV2) -> SourceResolutionInputRows {
28    let selector_certainty_flow_hedges =
29        collect_expression_domain_selector_certainty_flow_hedges(input);
30    collect_source_resolution_input_rows_with_flow_hedges(input, &selector_certainty_flow_hedges)
31}
32
33fn collect_source_resolution_input_rows_with_flow_hedges(
34    input: &EngineInputV2,
35    selector_certainty_flow_hedges: &ExpressionDomainSelectorCertaintyFlowHedgesV0,
36) -> SourceResolutionInputRows {
37    let mut expression_index = BTreeMap::new();
38    let mut style_index = BTreeMap::new();
39    let mut query_fragments = Vec::new();
40
41    for source in &input.sources {
42        for expression in &source.document.class_expressions {
43            expression_index.insert(expression.id.clone(), expression);
44            query_fragments.push(SourceResolutionQueryFragmentV0 {
45                query_id: expression.id.clone(),
46                expression_id: expression.id.clone(),
47                expression_kind: expression.kind.clone(),
48                style_file_path: expression.scss_module_path.clone(),
49            });
50        }
51    }
52
53    for style in &input.styles {
54        style_index.insert(style.file_path.clone(), style);
55    }
56
57    let mut fragments = Vec::new();
58    let mut match_fragments = Vec::new();
59    let mut candidates = Vec::new();
60    let mut evaluator_candidates = Vec::new();
61
62    for entry in &input.type_facts {
63        let Some(expression) = expression_index.get(&entry.expression_id) else {
64            continue;
65        };
66        let Some(style) = style_index.get(&expression.scss_module_path) else {
67            continue;
68        };
69
70        let selector_names = resolve_selector_names(style, &entry.facts);
71        let finite_values = finite_values_for_facts(&entry.facts);
72        let selector_certainty_projection = map_selector_certainty_projection(
73            &entry.facts,
74            selector_names.len(),
75            canonical_selector_count(style),
76            selector_certainty_flow_hedges
77                .get(&(entry.file_path.clone(), entry.expression_id.clone())),
78        );
79        let selector_certainty = selector_certainty_projection.certainty;
80        let selector_certainty_shape_label = selector_certainty_projection.shape_label;
81        let selector_certainty_shape_kind = selector_certainty_projection.shape_kind;
82        let value_certainty = map_value_certainty(&entry.facts);
83        let value_certainty_shape_kind = map_value_certainty_shape_kind(&entry.facts);
84        let value_certainty_shape_label = map_value_certainty_shape_label(&entry.facts);
85
86        fragments.push(SourceResolutionFragmentV0 {
87            query_id: entry.expression_id.clone(),
88            expression_id: entry.expression_id.clone(),
89            style_file_path: expression.scss_module_path.clone(),
90            value_certainty_shape_kind: value_certainty_shape_kind.clone(),
91            value_certainty_constraint_kind: entry.facts.constraint_kind.clone(),
92            value_prefix: entry.facts.prefix.clone(),
93            value_suffix: entry.facts.suffix.clone(),
94            value_min_len: entry.facts.min_len,
95            value_max_len: entry.facts.max_len,
96            value_char_must: entry.facts.char_must.clone(),
97            value_char_may: entry.facts.char_may.clone(),
98            value_may_include_other_chars: entry.facts.may_include_other_chars,
99        });
100
101        match_fragments.push(SourceResolutionMatchFragmentV0 {
102            query_id: entry.expression_id.clone(),
103            expression_id: entry.expression_id.clone(),
104            style_file_path: expression.scss_module_path.clone(),
105            selector_names: selector_names.clone(),
106            finite_values: finite_values.clone(),
107        });
108
109        let candidate = SourceResolutionCandidateV0 {
110            query_id: entry.expression_id.clone(),
111            expression_id: entry.expression_id.clone(),
112            style_file_path: expression.scss_module_path.clone(),
113            selector_names,
114            finite_values,
115            selector_certainty,
116            value_certainty,
117            selector_certainty_shape_kind,
118            selector_certainty_shape_label,
119            value_certainty_shape_kind,
120            value_certainty_shape_label,
121            selector_constraint_kind: entry.facts.constraint_kind.clone(),
122            value_certainty_constraint_kind: entry.facts.constraint_kind.clone(),
123            value_prefix: entry.facts.prefix.clone(),
124            value_suffix: entry.facts.suffix.clone(),
125            value_min_len: entry.facts.min_len,
126            value_max_len: entry.facts.max_len,
127            value_char_must: entry.facts.char_must.clone(),
128            value_char_may: entry.facts.char_may.clone(),
129            value_may_include_other_chars: entry.facts.may_include_other_chars,
130        };
131
132        candidates.push(candidate.clone());
133        evaluator_candidates.push(SourceResolutionEvaluatorCandidateV0 {
134            kind: "source-expression-resolution",
135            file_path: entry.file_path.clone(),
136            query_id: entry.expression_id.clone(),
137            payload: SourceResolutionEvaluatorCandidatePayloadV0 {
138                expression_id: entry.expression_id.clone(),
139                style_file_path: candidate.style_file_path.clone(),
140                selector_names: candidate.selector_names.clone(),
141                finite_values: candidate.finite_values.clone(),
142                selector_certainty: candidate.selector_certainty.clone(),
143                value_certainty: candidate.value_certainty.clone(),
144                selector_certainty_shape_kind: candidate.selector_certainty_shape_kind.clone(),
145                selector_certainty_shape_label: candidate.selector_certainty_shape_label.clone(),
146                value_certainty_shape_kind: candidate.value_certainty_shape_kind.clone(),
147                value_certainty_shape_label: candidate.value_certainty_shape_label.clone(),
148                selector_constraint_kind: candidate.selector_constraint_kind.clone(),
149                value_certainty_constraint_kind: candidate.value_certainty_constraint_kind.clone(),
150                value_prefix: candidate.value_prefix.clone(),
151                value_suffix: candidate.value_suffix.clone(),
152                value_min_len: candidate.value_min_len,
153                value_max_len: candidate.value_max_len,
154                value_char_must: candidate.value_char_must.clone(),
155                value_char_may: candidate.value_char_may.clone(),
156                value_may_include_other_chars: candidate.value_may_include_other_chars,
157            },
158        });
159    }
160
161    query_fragments.sort_by(|a, b| a.query_id.cmp(&b.query_id));
162    fragments.sort_by(|a, b| a.query_id.cmp(&b.query_id));
163    match_fragments.sort_by(|a, b| a.query_id.cmp(&b.query_id));
164    candidates.sort_by(|a, b| a.query_id.cmp(&b.query_id));
165    evaluator_candidates.sort_by(|a, b| a.query_id.cmp(&b.query_id));
166
167    SourceResolutionInputRows {
168        query_fragments,
169        fragments,
170        match_fragments,
171        candidates,
172        evaluator_candidates,
173    }
174}
175
176pub fn summarize_source_resolution_candidates_input(
177    input: &EngineInputV2,
178) -> SourceResolutionCandidatesV0 {
179    let rows = collect_source_resolution_input_rows(input);
180
181    SourceResolutionCandidatesV0 {
182        schema_version: "0",
183        input_version: input.version.clone(),
184        candidates: rows.candidates,
185    }
186}
187
188pub fn summarize_source_resolution_evaluator_candidates_input(
189    input: &EngineInputV2,
190) -> SourceResolutionEvaluatorCandidatesV0 {
191    let rows = collect_source_resolution_input_rows(input);
192
193    SourceResolutionEvaluatorCandidatesV0 {
194        schema_version: "0",
195        input_version: input.version.clone(),
196        results: rows.evaluator_candidates,
197    }
198}
199
200pub fn summarize_source_resolution_canonical_candidate_bundle_input(
201    input: &EngineInputV2,
202) -> SourceResolutionCanonicalCandidateBundleV0 {
203    let rows = collect_source_resolution_input_rows(input);
204
205    SourceResolutionCanonicalCandidateBundleV0 {
206        schema_version: "0",
207        input_version: input.version.clone(),
208        query_fragments: rows.query_fragments,
209        fragments: rows.fragments,
210        match_fragments: rows.match_fragments,
211        candidates: rows.candidates,
212    }
213}
214
215pub fn summarize_source_resolution_canonical_producer_signal_input(
216    input: &EngineInputV2,
217) -> SourceResolutionCanonicalProducerSignalV0 {
218    let rows = collect_source_resolution_input_rows(input);
219    let input_version = input.version.clone();
220
221    SourceResolutionCanonicalProducerSignalV0 {
222        schema_version: "0",
223        input_version: input_version.clone(),
224        canonical_bundle: SourceResolutionCanonicalCandidateBundleV0 {
225            schema_version: "0",
226            input_version: input_version.clone(),
227            query_fragments: rows.query_fragments.clone(),
228            fragments: rows.fragments.clone(),
229            match_fragments: rows.match_fragments.clone(),
230            candidates: rows.candidates.clone(),
231        },
232        evaluator_candidates: SourceResolutionEvaluatorCandidatesV0 {
233            schema_version: "0",
234            input_version,
235            results: rows.evaluator_candidates,
236        },
237    }
238}
239
240pub fn summarize_source_resolution_plan_input(
241    input: &EngineInputV2,
242) -> SourceResolutionPlanSummaryV0 {
243    let mut planned_expression_ids = Vec::new();
244    let mut expression_kind_counts = BTreeMap::new();
245    let mut distinct_style_file_paths = BTreeSet::new();
246    let mut symbol_ref_with_binding_count = 0usize;
247    let mut style_access_count = 0usize;
248    let mut style_access_path_depth_sum = 0usize;
249
250    for source in &input.sources {
251        for expression in &source.document.class_expressions {
252            planned_expression_ids.push(expression.id.clone());
253            distinct_style_file_paths.insert(expression.scss_module_path.clone());
254            *expression_kind_counts
255                .entry(expression.kind.clone())
256                .or_insert(0) += 1;
257
258            if expression.kind == "symbolRef" && expression.root_binding_decl_id.is_some() {
259                symbol_ref_with_binding_count += 1;
260            }
261
262            if expression.kind == "styleAccess" {
263                style_access_count += 1;
264                style_access_path_depth_sum += expression.access_path.as_ref().map_or(0, Vec::len);
265            }
266        }
267    }
268
269    SourceResolutionPlanSummaryV0 {
270        schema_version: "0",
271        input_version: input.version.clone(),
272        planned_expression_ids,
273        expression_kind_counts,
274        distinct_style_file_paths: distinct_style_file_paths.into_iter().collect(),
275        symbol_ref_with_binding_count,
276        style_access_count,
277        style_access_path_depth_sum,
278    }
279}
280
281pub fn summarize_source_resolution_fragments_input(
282    input: &EngineInputV2,
283) -> SourceResolutionFragmentsV0 {
284    let rows = collect_source_resolution_input_rows(input);
285
286    SourceResolutionFragmentsV0 {
287        schema_version: "0",
288        input_version: input.version.clone(),
289        fragments: rows.fragments,
290    }
291}
292
293pub fn summarize_source_resolution_query_fragments_input(
294    input: &EngineInputV2,
295) -> SourceResolutionQueryFragmentsV0 {
296    let rows = collect_source_resolution_input_rows(input);
297
298    SourceResolutionQueryFragmentsV0 {
299        schema_version: "0",
300        input_version: input.version.clone(),
301        fragments: rows.query_fragments,
302    }
303}
304
305pub fn summarize_source_resolution_match_fragments_input(
306    input: &EngineInputV2,
307) -> SourceResolutionMatchFragmentsV0 {
308    let rows = collect_source_resolution_input_rows(input);
309
310    SourceResolutionMatchFragmentsV0 {
311        schema_version: "0",
312        input_version: input.version.clone(),
313        fragments: rows.match_fragments,
314    }
315}
316
317#[cfg(test)]
318mod tests {
319    use super::{
320        summarize_source_resolution_candidates_input,
321        summarize_source_resolution_canonical_candidate_bundle_input,
322        summarize_source_resolution_canonical_producer_signal_input,
323        summarize_source_resolution_evaluator_candidates_input,
324        summarize_source_resolution_fragments_input,
325        summarize_source_resolution_match_fragments_input, summarize_source_resolution_plan_input,
326        summarize_source_resolution_query_fragments_input,
327    };
328    use crate::{
329        PositionV2, RangeV2, StyleSelectorV2, configure_nonconvergent_selector_certainty_fixture,
330        test_support::sample_input,
331    };
332
333    #[test]
334    fn builds_source_resolution_fragment_from_type_fact() {
335        let summary = summarize_source_resolution_fragments_input(&sample_input());
336
337        assert_eq!(summary.fragments.len(), 2);
338        let first = &summary.fragments[0];
339        assert_eq!(first.query_id, "expr-1");
340        assert_eq!(first.style_file_path, "/tmp/App.module.scss");
341        assert_eq!(first.value_certainty_shape_kind, "constrained");
342        assert_eq!(
343            first.value_certainty_constraint_kind.as_deref(),
344            Some("prefixSuffix")
345        );
346
347        let second = &summary.fragments[1];
348        assert_eq!(second.query_id, "expr-2");
349        assert_eq!(second.expression_id, "expr-2");
350        assert_eq!(second.style_file_path, "/tmp/Card.module.scss");
351        assert_eq!(second.value_certainty_shape_kind, "boundedFinite");
352        assert!(second.value_certainty_constraint_kind.is_none());
353    }
354
355    #[test]
356    fn builds_source_resolution_plan_from_input() {
357        let summary = summarize_source_resolution_plan_input(&sample_input());
358
359        assert_eq!(
360            summary.planned_expression_ids,
361            vec!["expr-1".to_string(), "expr-2".to_string()]
362        );
363        assert_eq!(
364            summary.distinct_style_file_paths,
365            vec![
366                "/tmp/App.module.scss".to_string(),
367                "/tmp/Card.module.scss".to_string()
368            ]
369        );
370        assert_eq!(summary.symbol_ref_with_binding_count, 1);
371        assert_eq!(summary.style_access_count, 1);
372        assert_eq!(summary.style_access_path_depth_sum, 2);
373    }
374
375    #[test]
376    fn builds_source_resolution_query_fragments_from_input() {
377        let summary = summarize_source_resolution_query_fragments_input(&sample_input());
378
379        assert_eq!(summary.fragments.len(), 2);
380        let first = &summary.fragments[0];
381        assert_eq!(first.query_id, "expr-1");
382        assert_eq!(first.expression_id, "expr-1");
383        assert_eq!(first.expression_kind, "symbolRef");
384        assert_eq!(first.style_file_path, "/tmp/App.module.scss");
385
386        let second = &summary.fragments[1];
387        assert_eq!(second.query_id, "expr-2");
388        assert_eq!(second.expression_kind, "styleAccess");
389        assert_eq!(second.style_file_path, "/tmp/Card.module.scss");
390    }
391
392    #[test]
393    fn builds_source_resolution_match_fragments_from_input() {
394        let summary = summarize_source_resolution_match_fragments_input(&sample_input());
395
396        assert_eq!(summary.fragments.len(), 2);
397        let first = &summary.fragments[0];
398        assert_eq!(first.query_id, "expr-1");
399        assert_eq!(first.expression_id, "expr-1");
400        assert_eq!(first.style_file_path, "/tmp/App.module.scss");
401        assert_eq!(first.selector_names, vec!["btn-active".to_string()]);
402        assert!(first.finite_values.is_none());
403
404        let second = &summary.fragments[1];
405        assert_eq!(second.query_id, "expr-2");
406        assert_eq!(second.style_file_path, "/tmp/Card.module.scss");
407        assert_eq!(second.selector_names, vec!["card-header".to_string()]);
408        assert_eq!(
409            second.finite_values,
410            Some(vec!["card-header".to_string(), "card-body".to_string()])
411        );
412    }
413
414    #[test]
415    fn builds_source_resolution_candidates_from_input() {
416        let summary = summarize_source_resolution_candidates_input(&sample_input());
417
418        assert_eq!(summary.candidates.len(), 2);
419        let first = &summary.candidates[0];
420        assert_eq!(first.query_id, "expr-1");
421        assert_eq!(first.expression_id, "expr-1");
422        assert_eq!(first.style_file_path, "/tmp/App.module.scss");
423        assert_eq!(first.selector_names, vec!["btn-active".to_string()]);
424        assert_eq!(first.selector_certainty, "inferred");
425        assert_eq!(first.selector_certainty_shape_kind, "constrained");
426        assert_eq!(
427            first.selector_certainty_shape_label,
428            "constrained edge selector set (1)"
429        );
430        assert_eq!(
431            first.selector_constraint_kind.as_deref(),
432            Some("prefixSuffix")
433        );
434        assert_eq!(first.value_certainty.as_deref(), Some("inferred"));
435        assert_eq!(first.value_certainty_shape_kind, "constrained");
436        assert_eq!(
437            first.value_certainty_shape_label,
438            "constrained prefix `btn-` + suffix `-active`"
439        );
440        assert_eq!(
441            first.value_certainty_constraint_kind.as_deref(),
442            Some("prefixSuffix")
443        );
444
445        let second = &summary.candidates[1];
446        assert_eq!(second.query_id, "expr-2");
447        assert_eq!(second.selector_names, vec!["card-header".to_string()]);
448        assert_eq!(second.selector_certainty, "inferred");
449        assert_eq!(second.selector_certainty_shape_kind, "boundedFinite");
450        assert_eq!(
451            second.selector_certainty_shape_label,
452            "bounded selector set (1)"
453        );
454        assert_eq!(second.value_certainty.as_deref(), Some("inferred"));
455        assert_eq!(second.value_certainty_shape_kind, "boundedFinite");
456        assert_eq!(second.value_certainty_shape_label, "bounded finite (2)");
457        assert_eq!(
458            second.finite_values,
459            Some(vec!["card-header".to_string(), "card-body".to_string()])
460        );
461    }
462
463    #[test]
464    fn source_resolution_applies_utf16_min_and_max_bounds_to_non_ascii_selectors() {
465        let original_e8_korean = unicode_length_input(
466            "카드-",
467            "-활성",
468            &["카드-활성", "카드-큰-활성", "카드-x-활성"],
469            Some(10),
470            None,
471        );
472        let original_e8_ascii = unicode_length_input(
473            "ab-",
474            "-cd",
475            &["ab-cd", "ab-x-cd", "ab-long-cd"],
476            Some(10),
477            None,
478        );
479        assert!(
480            summarize_source_resolution_candidates_input(&original_e8_korean).candidates[0]
481                .selector_names
482                .is_empty(),
483            "the original Korean E8 candidates are five, seven, and seven UTF-16 units"
484        );
485        assert_eq!(
486            summarize_source_resolution_candidates_input(&original_e8_ascii).candidates[0]
487                .selector_names,
488            vec!["ab-long-cd".to_string()],
489            "the original E8 ASCII control has a ten-unit third member and is not a structural twin"
490        );
491
492        let korean = unicode_length_input(
493            "카드-",
494            "-활성",
495            &["카드-활성", "카드-x-활성", "카드-1234-활성"],
496            Some(10),
497            None,
498        );
499        let ascii = unicode_length_input(
500            "ab-",
501            "-cd",
502            &["ab-cd", "ab-x-cd", "ab-1234-cd"],
503            Some(10),
504            None,
505        );
506
507        assert_eq!(
508            summarize_source_resolution_candidates_input(&korean).candidates[0].selector_names,
509            vec!["카드-1234-활성".to_string()]
510        );
511        assert_eq!(
512            summarize_source_resolution_candidates_input(&ascii).candidates[0].selector_names,
513            vec!["ab-1234-cd".to_string()]
514        );
515
516        let max_five = unicode_length_input(
517            "카드-",
518            "-활성",
519            &["카드-활성", "카드-x-활성", "카드-1234-활성"],
520            None,
521            Some(5),
522        );
523        assert_eq!(
524            summarize_source_resolution_candidates_input(&max_five).candidates[0].selector_names,
525            vec!["카드-활성".to_string()],
526            "the genuine five-code-unit member must pass while longer names are excluded"
527        );
528
529        let max_four = unicode_length_input(
530            "카드-",
531            "-활성",
532            &["카드-활성", "카드-x-활성", "카드-1234-활성"],
533            None,
534            Some(4),
535        );
536        assert!(
537            summarize_source_resolution_candidates_input(&max_four).candidates[0]
538                .selector_names
539                .is_empty(),
540            "a five-code-unit member must not pass a four-code-unit maximum"
541        );
542    }
543
544    #[test]
545    fn source_resolution_applies_utf16_bounds_to_exact_and_finite_values() {
546        let mut exact = unicode_length_input("카드-", "-활성", &["카드-활성"], Some(5), Some(5));
547        let exact_facts = &mut exact.type_facts[0].facts;
548        exact_facts.kind = "exact".to_string();
549        exact_facts.constraint_kind = None;
550        exact_facts.values = Some(vec!["카드-활성".to_string()]);
551        assert_eq!(
552            summarize_source_resolution_candidates_input(&exact).candidates[0].selector_names,
553            vec!["카드-활성".to_string()]
554        );
555
556        exact.type_facts[0].facts.max_len = Some(4);
557        assert!(
558            summarize_source_resolution_candidates_input(&exact).candidates[0]
559                .selector_names
560                .is_empty(),
561            "an exact five-code-unit value must not pass a four-code-unit maximum"
562        );
563
564        exact.type_facts[0].facts.max_len = Some(5);
565        exact.type_facts[0].facts.min_len = Some(6);
566        assert!(
567            summarize_source_resolution_candidates_input(&exact).candidates[0]
568                .selector_names
569                .is_empty(),
570            "an exact five-code-unit value must not pass a six-code-unit minimum"
571        );
572
573        let mut finite = unicode_length_input(
574            "카드-",
575            "-활성",
576            &["카드-활성", "카드-x-활성"],
577            Some(6),
578            Some(7),
579        );
580        let finite_facts = &mut finite.type_facts[0].facts;
581        finite_facts.kind = "finiteSet".to_string();
582        finite_facts.constraint_kind = None;
583        finite_facts.values = Some(vec!["카드-활성".to_string(), "카드-x-활성".to_string()]);
584        assert_eq!(
585            summarize_source_resolution_candidates_input(&finite).candidates[0].selector_names,
586            vec!["카드-x-활성".to_string()],
587            "the minimum excludes the five-unit value while the maximum admits the seven-unit value"
588        );
589
590        finite.type_facts[0].facts.max_len = Some(6);
591        assert!(
592            summarize_source_resolution_candidates_input(&finite).candidates[0]
593                .selector_names
594                .is_empty(),
595            "the finite seven-unit value must be excluded once the maximum drops below it"
596        );
597    }
598
599    #[test]
600    fn nonconverged_flow_hedge_demotes_source_resolution_product() {
601        let mut input = sample_input();
602        input.styles[0].document.selectors[0].name = "x".to_string();
603        input.styles[0].document.selectors[0].canonical_name = Some("x".to_string());
604        configure_nonconvergent_selector_certainty_fixture(&mut input.type_facts[0], "x");
605        let product = summarize_source_resolution_canonical_producer_signal_input(&input);
606        let candidate = &product.canonical_bundle.candidates[0];
607
608        assert_eq!(candidate.query_id, "expr-1");
609        assert_eq!(candidate.selector_certainty, "possible");
610        assert_eq!(candidate.selector_certainty_shape_kind, "unknown");
611        assert_eq!(candidate.selector_certainty_shape_label, "unknown");
612        assert_eq!(
613            product.evaluator_candidates.results[0]
614                .payload
615                .selector_certainty,
616            "possible"
617        );
618    }
619
620    #[test]
621    fn builds_source_resolution_evaluator_candidates() {
622        let summary = summarize_source_resolution_evaluator_candidates_input(&sample_input());
623
624        assert_eq!(summary.results.len(), 2);
625        let first = &summary.results[0];
626        assert_eq!(first.kind, "source-expression-resolution");
627        assert_eq!(first.file_path, "/tmp/App.tsx");
628        assert_eq!(first.query_id, "expr-1");
629        assert_eq!(first.payload.style_file_path, "/tmp/App.module.scss");
630        assert_eq!(first.payload.selector_certainty_shape_kind, "constrained");
631    }
632
633    #[test]
634    fn builds_source_resolution_canonical_candidate_bundle() {
635        let summary = summarize_source_resolution_canonical_candidate_bundle_input(&sample_input());
636
637        assert_eq!(summary.query_fragments.len(), 2);
638        assert_eq!(summary.fragments.len(), 2);
639        assert_eq!(summary.match_fragments.len(), 2);
640        assert_eq!(summary.candidates.len(), 2);
641    }
642
643    #[test]
644    fn builds_source_resolution_canonical_producer_signal() {
645        let summary = summarize_source_resolution_canonical_producer_signal_input(&sample_input());
646
647        assert_eq!(summary.canonical_bundle.candidates.len(), 2);
648        assert_eq!(summary.evaluator_candidates.results.len(), 2);
649        assert_eq!(summary.evaluator_candidates.results[0].query_id, "expr-1");
650    }
651
652    fn unicode_length_input(
653        prefix: &str,
654        suffix: &str,
655        selector_names: &[&str],
656        min_len: Option<crate::Utf16CodeUnitLengthV2>,
657        max_len: Option<crate::Utf16CodeUnitLengthV2>,
658    ) -> crate::EngineInputV2 {
659        let mut input = sample_input();
660        input.styles.truncate(1);
661        input.styles[0].document.selectors = selector_names
662            .iter()
663            .enumerate()
664            .map(|(index, name)| StyleSelectorV2 {
665                name: (*name).to_string(),
666                view_kind: "canonical".to_string(),
667                canonical_name: Some((*name).to_string()),
668                range: RangeV2 {
669                    start: PositionV2 {
670                        line: index,
671                        character: 0,
672                    },
673                    end: PositionV2 {
674                        line: index,
675                        character: name.encode_utf16().count(),
676                    },
677                },
678                nested_safety: Some("safe".to_string()),
679                composes: None,
680                bem_suffix: None,
681            })
682            .collect();
683        input.type_facts.truncate(1);
684        let facts = &mut input.type_facts[0].facts;
685        facts.prefix = Some(prefix.to_string());
686        facts.suffix = Some(suffix.to_string());
687        facts.min_len = min_len;
688        facts.max_len = max_len;
689        input
690    }
691}