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}