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, finite_values_for_facts,
11 map_selector_certainty, map_selector_certainty_shape_kind, map_selector_certainty_shape_label,
12 map_value_certainty, map_value_certainty_shape_kind, map_value_certainty_shape_label,
13 resolve_selector_names,
14};
15
16struct SourceResolutionInputRows {
17 query_fragments: Vec<SourceResolutionQueryFragmentV0>,
18 fragments: Vec<SourceResolutionFragmentV0>,
19 match_fragments: Vec<SourceResolutionMatchFragmentV0>,
20 candidates: Vec<SourceResolutionCandidateV0>,
21 evaluator_candidates: Vec<SourceResolutionEvaluatorCandidateV0>,
22}
23
24fn collect_source_resolution_input_rows(input: &EngineInputV2) -> SourceResolutionInputRows {
25 let mut expression_index = BTreeMap::new();
26 let mut style_index = BTreeMap::new();
27 let mut query_fragments = Vec::new();
28
29 for source in &input.sources {
30 for expression in &source.document.class_expressions {
31 expression_index.insert(expression.id.clone(), expression);
32 query_fragments.push(SourceResolutionQueryFragmentV0 {
33 query_id: expression.id.clone(),
34 expression_id: expression.id.clone(),
35 expression_kind: expression.kind.clone(),
36 style_file_path: expression.scss_module_path.clone(),
37 });
38 }
39 }
40
41 for style in &input.styles {
42 style_index.insert(style.file_path.clone(), style);
43 }
44
45 let mut fragments = Vec::new();
46 let mut match_fragments = Vec::new();
47 let mut candidates = Vec::new();
48 let mut evaluator_candidates = Vec::new();
49
50 for entry in &input.type_facts {
51 let Some(expression) = expression_index.get(&entry.expression_id) else {
52 continue;
53 };
54 let Some(style) = style_index.get(&expression.scss_module_path) else {
55 continue;
56 };
57
58 let selector_names = resolve_selector_names(style, &entry.facts);
59 let finite_values = finite_values_for_facts(&entry.facts);
60 let selector_certainty = map_selector_certainty(
61 &entry.facts,
62 selector_names.len(),
63 canonical_selector_count(style),
64 );
65 let selector_certainty_shape_label = map_selector_certainty_shape_label(
66 &entry.facts,
67 selector_names.len(),
68 canonical_selector_count(style),
69 );
70 let selector_certainty_shape_kind = map_selector_certainty_shape_kind(
71 &entry.facts,
72 selector_names.len(),
73 canonical_selector_count(style),
74 );
75 let value_certainty = map_value_certainty(&entry.facts);
76 let value_certainty_shape_kind = map_value_certainty_shape_kind(&entry.facts);
77 let value_certainty_shape_label = map_value_certainty_shape_label(&entry.facts);
78
79 fragments.push(SourceResolutionFragmentV0 {
80 query_id: entry.expression_id.clone(),
81 expression_id: entry.expression_id.clone(),
82 style_file_path: expression.scss_module_path.clone(),
83 value_certainty_shape_kind: value_certainty_shape_kind.clone(),
84 value_certainty_constraint_kind: entry.facts.constraint_kind.clone(),
85 value_prefix: entry.facts.prefix.clone(),
86 value_suffix: entry.facts.suffix.clone(),
87 value_min_len: entry.facts.min_len,
88 value_max_len: entry.facts.max_len,
89 value_char_must: entry.facts.char_must.clone(),
90 value_char_may: entry.facts.char_may.clone(),
91 value_may_include_other_chars: entry.facts.may_include_other_chars,
92 });
93
94 match_fragments.push(SourceResolutionMatchFragmentV0 {
95 query_id: entry.expression_id.clone(),
96 expression_id: entry.expression_id.clone(),
97 style_file_path: expression.scss_module_path.clone(),
98 selector_names: selector_names.clone(),
99 finite_values: finite_values.clone(),
100 });
101
102 let candidate = SourceResolutionCandidateV0 {
103 query_id: entry.expression_id.clone(),
104 expression_id: entry.expression_id.clone(),
105 style_file_path: expression.scss_module_path.clone(),
106 selector_names,
107 finite_values,
108 selector_certainty,
109 value_certainty,
110 selector_certainty_shape_kind,
111 selector_certainty_shape_label,
112 value_certainty_shape_kind,
113 value_certainty_shape_label,
114 selector_constraint_kind: entry.facts.constraint_kind.clone(),
115 value_certainty_constraint_kind: entry.facts.constraint_kind.clone(),
116 value_prefix: entry.facts.prefix.clone(),
117 value_suffix: entry.facts.suffix.clone(),
118 value_min_len: entry.facts.min_len,
119 value_max_len: entry.facts.max_len,
120 value_char_must: entry.facts.char_must.clone(),
121 value_char_may: entry.facts.char_may.clone(),
122 value_may_include_other_chars: entry.facts.may_include_other_chars,
123 };
124
125 candidates.push(candidate.clone());
126 evaluator_candidates.push(SourceResolutionEvaluatorCandidateV0 {
127 kind: "source-expression-resolution",
128 file_path: entry.file_path.clone(),
129 query_id: entry.expression_id.clone(),
130 payload: SourceResolutionEvaluatorCandidatePayloadV0 {
131 expression_id: entry.expression_id.clone(),
132 style_file_path: candidate.style_file_path.clone(),
133 selector_names: candidate.selector_names.clone(),
134 finite_values: candidate.finite_values.clone(),
135 selector_certainty: candidate.selector_certainty.clone(),
136 value_certainty: candidate.value_certainty.clone(),
137 selector_certainty_shape_kind: candidate.selector_certainty_shape_kind.clone(),
138 selector_certainty_shape_label: candidate.selector_certainty_shape_label.clone(),
139 value_certainty_shape_kind: candidate.value_certainty_shape_kind.clone(),
140 value_certainty_shape_label: candidate.value_certainty_shape_label.clone(),
141 selector_constraint_kind: candidate.selector_constraint_kind.clone(),
142 value_certainty_constraint_kind: candidate.value_certainty_constraint_kind.clone(),
143 value_prefix: candidate.value_prefix.clone(),
144 value_suffix: candidate.value_suffix.clone(),
145 value_min_len: candidate.value_min_len,
146 value_max_len: candidate.value_max_len,
147 value_char_must: candidate.value_char_must.clone(),
148 value_char_may: candidate.value_char_may.clone(),
149 value_may_include_other_chars: candidate.value_may_include_other_chars,
150 },
151 });
152 }
153
154 query_fragments.sort_by(|a, b| a.query_id.cmp(&b.query_id));
155 fragments.sort_by(|a, b| a.query_id.cmp(&b.query_id));
156 match_fragments.sort_by(|a, b| a.query_id.cmp(&b.query_id));
157 candidates.sort_by(|a, b| a.query_id.cmp(&b.query_id));
158 evaluator_candidates.sort_by(|a, b| a.query_id.cmp(&b.query_id));
159
160 SourceResolutionInputRows {
161 query_fragments,
162 fragments,
163 match_fragments,
164 candidates,
165 evaluator_candidates,
166 }
167}
168
169pub fn summarize_source_resolution_candidates_input(
170 input: &EngineInputV2,
171) -> SourceResolutionCandidatesV0 {
172 let rows = collect_source_resolution_input_rows(input);
173
174 SourceResolutionCandidatesV0 {
175 schema_version: "0",
176 input_version: input.version.clone(),
177 candidates: rows.candidates,
178 }
179}
180
181pub fn summarize_source_resolution_evaluator_candidates_input(
182 input: &EngineInputV2,
183) -> SourceResolutionEvaluatorCandidatesV0 {
184 let rows = collect_source_resolution_input_rows(input);
185
186 SourceResolutionEvaluatorCandidatesV0 {
187 schema_version: "0",
188 input_version: input.version.clone(),
189 results: rows.evaluator_candidates,
190 }
191}
192
193pub fn summarize_source_resolution_canonical_candidate_bundle_input(
194 input: &EngineInputV2,
195) -> SourceResolutionCanonicalCandidateBundleV0 {
196 let rows = collect_source_resolution_input_rows(input);
197
198 SourceResolutionCanonicalCandidateBundleV0 {
199 schema_version: "0",
200 input_version: input.version.clone(),
201 query_fragments: rows.query_fragments,
202 fragments: rows.fragments,
203 match_fragments: rows.match_fragments,
204 candidates: rows.candidates,
205 }
206}
207
208pub fn summarize_source_resolution_canonical_producer_signal_input(
209 input: &EngineInputV2,
210) -> SourceResolutionCanonicalProducerSignalV0 {
211 let rows = collect_source_resolution_input_rows(input);
212 let input_version = input.version.clone();
213
214 SourceResolutionCanonicalProducerSignalV0 {
215 schema_version: "0",
216 input_version: input_version.clone(),
217 canonical_bundle: SourceResolutionCanonicalCandidateBundleV0 {
218 schema_version: "0",
219 input_version: input_version.clone(),
220 query_fragments: rows.query_fragments.clone(),
221 fragments: rows.fragments.clone(),
222 match_fragments: rows.match_fragments.clone(),
223 candidates: rows.candidates.clone(),
224 },
225 evaluator_candidates: SourceResolutionEvaluatorCandidatesV0 {
226 schema_version: "0",
227 input_version,
228 results: rows.evaluator_candidates,
229 },
230 }
231}
232
233pub fn summarize_source_resolution_plan_input(
234 input: &EngineInputV2,
235) -> SourceResolutionPlanSummaryV0 {
236 let mut planned_expression_ids = Vec::new();
237 let mut expression_kind_counts = BTreeMap::new();
238 let mut distinct_style_file_paths = BTreeSet::new();
239 let mut symbol_ref_with_binding_count = 0usize;
240 let mut style_access_count = 0usize;
241 let mut style_access_path_depth_sum = 0usize;
242
243 for source in &input.sources {
244 for expression in &source.document.class_expressions {
245 planned_expression_ids.push(expression.id.clone());
246 distinct_style_file_paths.insert(expression.scss_module_path.clone());
247 *expression_kind_counts
248 .entry(expression.kind.clone())
249 .or_insert(0) += 1;
250
251 if expression.kind == "symbolRef" && expression.root_binding_decl_id.is_some() {
252 symbol_ref_with_binding_count += 1;
253 }
254
255 if expression.kind == "styleAccess" {
256 style_access_count += 1;
257 style_access_path_depth_sum += expression.access_path.as_ref().map_or(0, Vec::len);
258 }
259 }
260 }
261
262 SourceResolutionPlanSummaryV0 {
263 schema_version: "0",
264 input_version: input.version.clone(),
265 planned_expression_ids,
266 expression_kind_counts,
267 distinct_style_file_paths: distinct_style_file_paths.into_iter().collect(),
268 symbol_ref_with_binding_count,
269 style_access_count,
270 style_access_path_depth_sum,
271 }
272}
273
274pub fn summarize_source_resolution_fragments_input(
275 input: &EngineInputV2,
276) -> SourceResolutionFragmentsV0 {
277 let rows = collect_source_resolution_input_rows(input);
278
279 SourceResolutionFragmentsV0 {
280 schema_version: "0",
281 input_version: input.version.clone(),
282 fragments: rows.fragments,
283 }
284}
285
286pub fn summarize_source_resolution_query_fragments_input(
287 input: &EngineInputV2,
288) -> SourceResolutionQueryFragmentsV0 {
289 let rows = collect_source_resolution_input_rows(input);
290
291 SourceResolutionQueryFragmentsV0 {
292 schema_version: "0",
293 input_version: input.version.clone(),
294 fragments: rows.query_fragments,
295 }
296}
297
298pub fn summarize_source_resolution_match_fragments_input(
299 input: &EngineInputV2,
300) -> SourceResolutionMatchFragmentsV0 {
301 let rows = collect_source_resolution_input_rows(input);
302
303 SourceResolutionMatchFragmentsV0 {
304 schema_version: "0",
305 input_version: input.version.clone(),
306 fragments: rows.match_fragments,
307 }
308}
309
310#[cfg(test)]
311mod tests {
312 use super::{
313 summarize_source_resolution_candidates_input,
314 summarize_source_resolution_canonical_candidate_bundle_input,
315 summarize_source_resolution_canonical_producer_signal_input,
316 summarize_source_resolution_evaluator_candidates_input,
317 summarize_source_resolution_fragments_input,
318 summarize_source_resolution_match_fragments_input, summarize_source_resolution_plan_input,
319 summarize_source_resolution_query_fragments_input,
320 };
321 use crate::test_support::sample_input;
322
323 #[test]
324 fn builds_source_resolution_fragment_from_type_fact() {
325 let summary = summarize_source_resolution_fragments_input(&sample_input());
326
327 assert_eq!(summary.fragments.len(), 2);
328 let first = &summary.fragments[0];
329 assert_eq!(first.query_id, "expr-1");
330 assert_eq!(first.style_file_path, "/tmp/App.module.scss");
331 assert_eq!(first.value_certainty_shape_kind, "constrained");
332 assert_eq!(
333 first.value_certainty_constraint_kind.as_deref(),
334 Some("prefixSuffix")
335 );
336
337 let second = &summary.fragments[1];
338 assert_eq!(second.query_id, "expr-2");
339 assert_eq!(second.expression_id, "expr-2");
340 assert_eq!(second.style_file_path, "/tmp/Card.module.scss");
341 assert_eq!(second.value_certainty_shape_kind, "boundedFinite");
342 assert!(second.value_certainty_constraint_kind.is_none());
343 }
344
345 #[test]
346 fn builds_source_resolution_plan_from_input() {
347 let summary = summarize_source_resolution_plan_input(&sample_input());
348
349 assert_eq!(
350 summary.planned_expression_ids,
351 vec!["expr-1".to_string(), "expr-2".to_string()]
352 );
353 assert_eq!(
354 summary.distinct_style_file_paths,
355 vec![
356 "/tmp/App.module.scss".to_string(),
357 "/tmp/Card.module.scss".to_string()
358 ]
359 );
360 assert_eq!(summary.symbol_ref_with_binding_count, 1);
361 assert_eq!(summary.style_access_count, 1);
362 assert_eq!(summary.style_access_path_depth_sum, 2);
363 }
364
365 #[test]
366 fn builds_source_resolution_query_fragments_from_input() {
367 let summary = summarize_source_resolution_query_fragments_input(&sample_input());
368
369 assert_eq!(summary.fragments.len(), 2);
370 let first = &summary.fragments[0];
371 assert_eq!(first.query_id, "expr-1");
372 assert_eq!(first.expression_id, "expr-1");
373 assert_eq!(first.expression_kind, "symbolRef");
374 assert_eq!(first.style_file_path, "/tmp/App.module.scss");
375
376 let second = &summary.fragments[1];
377 assert_eq!(second.query_id, "expr-2");
378 assert_eq!(second.expression_kind, "styleAccess");
379 assert_eq!(second.style_file_path, "/tmp/Card.module.scss");
380 }
381
382 #[test]
383 fn builds_source_resolution_match_fragments_from_input() {
384 let summary = summarize_source_resolution_match_fragments_input(&sample_input());
385
386 assert_eq!(summary.fragments.len(), 2);
387 let first = &summary.fragments[0];
388 assert_eq!(first.query_id, "expr-1");
389 assert_eq!(first.expression_id, "expr-1");
390 assert_eq!(first.style_file_path, "/tmp/App.module.scss");
391 assert_eq!(first.selector_names, vec!["btn-active".to_string()]);
392 assert!(first.finite_values.is_none());
393
394 let second = &summary.fragments[1];
395 assert_eq!(second.query_id, "expr-2");
396 assert_eq!(second.style_file_path, "/tmp/Card.module.scss");
397 assert_eq!(second.selector_names, vec!["card-header".to_string()]);
398 assert_eq!(
399 second.finite_values,
400 Some(vec!["card-header".to_string(), "card-body".to_string()])
401 );
402 }
403
404 #[test]
405 fn builds_source_resolution_candidates_from_input() {
406 let summary = summarize_source_resolution_candidates_input(&sample_input());
407
408 assert_eq!(summary.candidates.len(), 2);
409 let first = &summary.candidates[0];
410 assert_eq!(first.query_id, "expr-1");
411 assert_eq!(first.expression_id, "expr-1");
412 assert_eq!(first.style_file_path, "/tmp/App.module.scss");
413 assert_eq!(first.selector_names, vec!["btn-active".to_string()]);
414 assert_eq!(first.selector_certainty, "exact");
415 assert_eq!(first.selector_certainty_shape_kind, "exact");
416 assert_eq!(first.selector_certainty_shape_label, "exact");
417 assert_eq!(
418 first.selector_constraint_kind.as_deref(),
419 Some("prefixSuffix")
420 );
421 assert_eq!(first.value_certainty.as_deref(), Some("inferred"));
422 assert_eq!(first.value_certainty_shape_kind, "constrained");
423 assert_eq!(
424 first.value_certainty_shape_label,
425 "constrained prefix `btn-` + suffix `-active`"
426 );
427 assert_eq!(
428 first.value_certainty_constraint_kind.as_deref(),
429 Some("prefixSuffix")
430 );
431
432 let second = &summary.candidates[1];
433 assert_eq!(second.query_id, "expr-2");
434 assert_eq!(second.selector_names, vec!["card-header".to_string()]);
435 assert_eq!(second.selector_certainty, "inferred");
436 assert_eq!(second.selector_certainty_shape_kind, "boundedFinite");
437 assert_eq!(
438 second.selector_certainty_shape_label,
439 "bounded selector set (1)"
440 );
441 assert_eq!(second.value_certainty.as_deref(), Some("inferred"));
442 assert_eq!(second.value_certainty_shape_kind, "boundedFinite");
443 assert_eq!(second.value_certainty_shape_label, "bounded finite (2)");
444 assert_eq!(
445 second.finite_values,
446 Some(vec!["card-header".to_string(), "card-body".to_string()])
447 );
448 }
449
450 #[test]
451 fn builds_source_resolution_evaluator_candidates() {
452 let summary = summarize_source_resolution_evaluator_candidates_input(&sample_input());
453
454 assert_eq!(summary.results.len(), 2);
455 let first = &summary.results[0];
456 assert_eq!(first.kind, "source-expression-resolution");
457 assert_eq!(first.file_path, "/tmp/App.tsx");
458 assert_eq!(first.query_id, "expr-1");
459 assert_eq!(first.payload.style_file_path, "/tmp/App.module.scss");
460 assert_eq!(first.payload.selector_certainty_shape_kind, "exact");
461 }
462
463 #[test]
464 fn builds_source_resolution_canonical_candidate_bundle() {
465 let summary = summarize_source_resolution_canonical_candidate_bundle_input(&sample_input());
466
467 assert_eq!(summary.query_fragments.len(), 2);
468 assert_eq!(summary.fragments.len(), 2);
469 assert_eq!(summary.match_fragments.len(), 2);
470 assert_eq!(summary.candidates.len(), 2);
471 }
472
473 #[test]
474 fn builds_source_resolution_canonical_producer_signal() {
475 let summary = summarize_source_resolution_canonical_producer_signal_input(&sample_input());
476
477 assert_eq!(summary.canonical_bundle.candidates.len(), 2);
478 assert_eq!(summary.evaluator_candidates.results.len(), 2);
479 assert_eq!(summary.evaluator_candidates.results[0].query_id, "expr-1");
480 }
481}