1use std::collections::{BTreeMap, BTreeSet};
2
3use engine_input_producers::{
4 EngineInputV2, SourceResolutionCandidateV0, SourceResolutionCanonicalProducerSignalV0,
5 SourceResolutionQueryFragmentV0, SourceResolutionQueryFragmentsV0,
6 summarize_source_resolution_canonical_producer_signal_input,
7 summarize_source_resolution_query_fragments_input,
8};
9use serde::{Deserialize, Serialize};
10
11#[derive(Debug, Serialize)]
12#[serde(rename_all = "camelCase")]
13pub struct OmenaResolverBoundarySummaryV0 {
14 pub schema_version: &'static str,
15 pub product: &'static str,
16 pub resolver_name: &'static str,
17 pub input_version: String,
18 pub delegated_source_resolution_products: Vec<&'static str>,
19 pub resolver_owned_products: Vec<&'static str>,
20 pub source_resolution_query_count: usize,
21 pub source_resolution_candidate_count: usize,
22 pub source_resolution_evaluator_candidate_count: usize,
23 pub module_graph_module_count: usize,
24 pub module_graph_source_expression_edge_count: usize,
25 pub runtime_query_module_count: usize,
26 pub runtime_query_ready_module_count: usize,
27 pub source_resolution_runtime_expression_count: usize,
28 pub source_resolution_runtime_resolved_expression_count: usize,
29 pub ready_surfaces: Vec<&'static str>,
30 pub cme_coupled_surfaces: Vec<&'static str>,
31 pub next_decoupling_targets: Vec<&'static str>,
32}
33
34#[derive(Debug, Clone, PartialEq, Eq, Deserialize, Serialize)]
35#[serde(rename_all = "camelCase")]
36pub struct OmenaResolverModuleGraphSummaryV0 {
37 pub schema_version: String,
38 pub product: String,
39 pub input_version: String,
40 pub module_count: usize,
41 pub source_expression_edge_count: usize,
42 pub type_fact_edge_count: usize,
43 pub selector_count: usize,
44 pub unresolved_type_fact_count: usize,
45 pub modules: Vec<OmenaResolverModuleGraphModuleV0>,
46 pub unresolved_type_fact_expression_ids: Vec<String>,
47}
48
49#[derive(Debug, Clone, PartialEq, Eq, Deserialize, Serialize)]
50#[serde(rename_all = "camelCase")]
51pub struct OmenaResolverModuleGraphModuleV0 {
52 pub style_file_path: String,
53 pub source_expression_ids: Vec<String>,
54 pub source_expression_kinds: Vec<String>,
55 pub type_fact_expression_ids: Vec<String>,
56 pub selector_names: Vec<String>,
57 pub canonical_selector_names: Vec<String>,
58 pub has_source_input: bool,
59 pub has_style_input: bool,
60 pub has_type_fact_input: bool,
61}
62
63#[derive(Debug, Clone, PartialEq, Eq, Serialize)]
64#[serde(rename_all = "camelCase")]
65pub struct OmenaResolverRuntimeQueryBoundarySummaryV0 {
66 pub schema_version: &'static str,
67 pub product: &'static str,
68 pub input_product: String,
69 pub input_version: String,
70 pub module_query_count: usize,
71 pub fully_resolvable_module_count: usize,
72 pub source_only_module_count: usize,
73 pub style_only_module_count: usize,
74 pub unresolved_type_fact_count: usize,
75 pub runtime_capabilities: Vec<&'static str>,
76 pub blocking_gaps: Vec<&'static str>,
77 pub module_queries: Vec<OmenaResolverRuntimeModuleQueryV0>,
78}
79
80#[derive(Debug, Clone, PartialEq, Eq, Serialize)]
81#[serde(rename_all = "camelCase")]
82pub struct OmenaResolverRuntimeModuleQueryV0 {
83 pub style_file_path: String,
84 pub source_expression_ids: Vec<String>,
85 pub type_fact_expression_ids: Vec<String>,
86 pub selector_names: Vec<String>,
87 pub canonical_selector_names: Vec<String>,
88 pub can_resolve_source_expressions: bool,
89 pub can_check_type_fact_edges: bool,
90 pub can_query_selector_names: bool,
91 pub status: &'static str,
92}
93
94#[derive(Debug, Clone, PartialEq, Eq, Serialize)]
95#[serde(rename_all = "camelCase")]
96pub struct OmenaResolverSourceResolutionRuntimeIndexV0 {
97 pub schema_version: &'static str,
98 pub product: &'static str,
99 pub input_product: &'static str,
100 pub input_version: String,
101 pub expression_count: usize,
102 pub resolved_expression_count: usize,
103 pub unresolved_expression_count: usize,
104 pub blocking_gaps: Vec<&'static str>,
105 pub entries: Vec<OmenaResolverSourceResolutionRuntimeEntryV0>,
106}
107
108#[derive(Debug, Clone, PartialEq, Eq, Serialize)]
109#[serde(rename_all = "camelCase")]
110pub struct OmenaResolverSourceResolutionRuntimeEntryV0 {
111 pub query_id: String,
112 pub expression_id: String,
113 pub expression_kind: String,
114 pub style_file_path: String,
115 pub selector_names: Vec<String>,
116 pub finite_values: Option<Vec<String>>,
117 pub selector_certainty: String,
118 pub value_certainty: Option<String>,
119 pub selector_certainty_shape_kind: String,
120 pub value_certainty_shape_kind: String,
121 pub has_selector_match: bool,
122 pub has_finite_values: bool,
123 pub can_resolve_source_expression: bool,
124 pub status: &'static str,
125}
126
127#[derive(Debug, Default)]
128struct ModuleGraphAccumulator {
129 source_expression_ids: BTreeSet<String>,
130 source_expression_kinds: BTreeSet<String>,
131 type_fact_expression_ids: BTreeSet<String>,
132 selector_names: BTreeSet<String>,
133 canonical_selector_names: BTreeSet<String>,
134 has_source_input: bool,
135 has_style_input: bool,
136 has_type_fact_input: bool,
137}
138
139pub fn summarize_omena_resolver_boundary(input: &EngineInputV2) -> OmenaResolverBoundarySummaryV0 {
140 let canonical_signal = summarize_omena_resolver_canonical_producer_signal(input);
141 let module_graph = summarize_omena_resolver_module_graph_index(input);
142 let runtime_query = summarize_omena_resolver_runtime_query_boundary(&module_graph);
143 let source_resolution_runtime = summarize_omena_resolver_source_resolution_runtime(input);
144
145 OmenaResolverBoundarySummaryV0 {
146 schema_version: "0",
147 product: "omena-resolver.boundary",
148 resolver_name: "omena-resolver",
149 input_version: input.version.clone(),
150 delegated_source_resolution_products: vec![
151 "engine-input-producers.source-resolution-query-fragments",
152 "engine-input-producers.source-resolution-canonical-producer",
153 ],
154 resolver_owned_products: vec![
155 "omena-resolver.module-graph-index",
156 "omena-resolver.runtime-query-boundary",
157 "omena-resolver.source-resolution-runtime-index",
158 ],
159 source_resolution_query_count: canonical_signal.canonical_bundle.query_fragments.len(),
160 source_resolution_candidate_count: canonical_signal.canonical_bundle.candidates.len(),
161 source_resolution_evaluator_candidate_count: canonical_signal
162 .evaluator_candidates
163 .results
164 .len(),
165 module_graph_module_count: module_graph.module_count,
166 module_graph_source_expression_edge_count: module_graph.source_expression_edge_count,
167 runtime_query_module_count: runtime_query.module_query_count,
168 runtime_query_ready_module_count: runtime_query.fully_resolvable_module_count,
169 source_resolution_runtime_expression_count: source_resolution_runtime.expression_count,
170 source_resolution_runtime_resolved_expression_count: source_resolution_runtime
171 .resolved_expression_count,
172 ready_surfaces: vec![
173 "resolverBoundarySummary",
174 "resolverModuleGraphIndex",
175 "resolverRuntimeQueryBoundary",
176 "resolverSourceResolutionRuntimeIndex",
177 "sourceResolutionQueryFragments",
178 "sourceResolutionCanonicalProducerSignal",
179 ],
180 cme_coupled_surfaces: vec!["EngineInputV2", "producerSourceResolutionRows"],
181 next_decoupling_targets: vec!["specifierResolutionRuntime", "tsconfigPathMapping"],
182 }
183}
184
185pub fn summarize_omena_resolver_module_graph_index(
186 input: &EngineInputV2,
187) -> OmenaResolverModuleGraphSummaryV0 {
188 let mut modules = BTreeMap::<String, ModuleGraphAccumulator>::new();
189 let mut expression_to_style_path = BTreeMap::<String, String>::new();
190 let mut source_expression_edge_count = 0usize;
191 let mut type_fact_edge_count = 0usize;
192 let mut selector_count = 0usize;
193 let mut unresolved_type_fact_expression_ids = BTreeSet::<String>::new();
194
195 for source in &input.sources {
196 for expression in &source.document.class_expressions {
197 source_expression_edge_count += 1;
198 expression_to_style_path
199 .insert(expression.id.clone(), expression.scss_module_path.clone());
200 let module = modules
201 .entry(expression.scss_module_path.clone())
202 .or_default();
203 module.has_source_input = true;
204 module.source_expression_ids.insert(expression.id.clone());
205 module
206 .source_expression_kinds
207 .insert(expression.kind.clone());
208 }
209 }
210
211 for style in &input.styles {
212 let module = modules.entry(style.file_path.clone()).or_default();
213 module.has_style_input = true;
214 for selector in &style.document.selectors {
215 selector_count += 1;
216 module.selector_names.insert(selector.name.clone());
217 if let Some(canonical_name) = &selector.canonical_name {
218 module
219 .canonical_selector_names
220 .insert(canonical_name.clone());
221 }
222 }
223 }
224
225 for type_fact in &input.type_facts {
226 if let Some(style_file_path) = expression_to_style_path.get(&type_fact.expression_id) {
227 type_fact_edge_count += 1;
228 let module = modules.entry(style_file_path.clone()).or_default();
229 module.has_type_fact_input = true;
230 module
231 .type_fact_expression_ids
232 .insert(type_fact.expression_id.clone());
233 } else {
234 unresolved_type_fact_expression_ids.insert(type_fact.expression_id.clone());
235 }
236 }
237
238 let modules = modules
239 .into_iter()
240 .map(
241 |(style_file_path, module)| OmenaResolverModuleGraphModuleV0 {
242 style_file_path,
243 source_expression_ids: module.source_expression_ids.into_iter().collect(),
244 source_expression_kinds: module.source_expression_kinds.into_iter().collect(),
245 type_fact_expression_ids: module.type_fact_expression_ids.into_iter().collect(),
246 selector_names: module.selector_names.into_iter().collect(),
247 canonical_selector_names: module.canonical_selector_names.into_iter().collect(),
248 has_source_input: module.has_source_input,
249 has_style_input: module.has_style_input,
250 has_type_fact_input: module.has_type_fact_input,
251 },
252 )
253 .collect::<Vec<_>>();
254 let unresolved_type_fact_expression_ids = unresolved_type_fact_expression_ids
255 .into_iter()
256 .collect::<Vec<_>>();
257
258 OmenaResolverModuleGraphSummaryV0 {
259 schema_version: "0".to_string(),
260 product: "omena-resolver.module-graph-index".to_string(),
261 input_version: input.version.clone(),
262 module_count: modules.len(),
263 source_expression_edge_count,
264 type_fact_edge_count,
265 selector_count,
266 unresolved_type_fact_count: unresolved_type_fact_expression_ids.len(),
267 modules,
268 unresolved_type_fact_expression_ids,
269 }
270}
271
272pub fn summarize_omena_resolver_runtime_query_boundary(
273 module_graph: &OmenaResolverModuleGraphSummaryV0,
274) -> OmenaResolverRuntimeQueryBoundarySummaryV0 {
275 let module_queries = module_graph
276 .modules
277 .iter()
278 .map(runtime_module_query_from_graph_module)
279 .collect::<Vec<_>>();
280 let fully_resolvable_module_count = module_queries
281 .iter()
282 .filter(|module| module.status == "ready")
283 .count();
284 let source_only_module_count = module_graph
285 .modules
286 .iter()
287 .filter(|module| module.has_source_input && !module.has_style_input)
288 .count();
289 let style_only_module_count = module_graph
290 .modules
291 .iter()
292 .filter(|module| module.has_style_input && !module.has_source_input)
293 .count();
294 let mut blocking_gaps = Vec::new();
295
296 if module_graph.module_count == 0 {
297 blocking_gaps.push("emptyModuleGraph");
298 }
299 if fully_resolvable_module_count < module_graph.module_count {
300 blocking_gaps.push("partialModuleCoverage");
301 }
302 if module_graph.unresolved_type_fact_count > 0 {
303 blocking_gaps.push("unresolvedTypeFactEdges");
304 }
305
306 OmenaResolverRuntimeQueryBoundarySummaryV0 {
307 schema_version: "0",
308 product: "omena-resolver.runtime-query-boundary",
309 input_product: module_graph.product.clone(),
310 input_version: module_graph.input_version.clone(),
311 module_query_count: module_queries.len(),
312 fully_resolvable_module_count,
313 source_only_module_count,
314 style_only_module_count,
315 unresolved_type_fact_count: module_graph.unresolved_type_fact_count,
316 runtime_capabilities: vec![
317 "moduleLookupByStylePath",
318 "sourceExpressionEdgeLookup",
319 "typeFactEdgeLookup",
320 "selectorNameLookup",
321 ],
322 blocking_gaps,
323 module_queries,
324 }
325}
326
327pub fn query_omena_resolver_runtime_module(
328 module_graph: &OmenaResolverModuleGraphSummaryV0,
329 style_file_path: &str,
330) -> Option<OmenaResolverRuntimeModuleQueryV0> {
331 module_graph
332 .modules
333 .iter()
334 .find(|module| module.style_file_path == style_file_path)
335 .map(runtime_module_query_from_graph_module)
336}
337
338fn runtime_module_query_from_graph_module(
339 module: &OmenaResolverModuleGraphModuleV0,
340) -> OmenaResolverRuntimeModuleQueryV0 {
341 OmenaResolverRuntimeModuleQueryV0 {
342 style_file_path: module.style_file_path.clone(),
343 source_expression_ids: module.source_expression_ids.clone(),
344 type_fact_expression_ids: module.type_fact_expression_ids.clone(),
345 selector_names: module.selector_names.clone(),
346 canonical_selector_names: module.canonical_selector_names.clone(),
347 can_resolve_source_expressions: module.has_source_input && module.has_style_input,
348 can_check_type_fact_edges: module.has_source_input && module.has_type_fact_input,
349 can_query_selector_names: module.has_style_input,
350 status: module_runtime_status(module),
351 }
352}
353
354fn module_runtime_status(module: &OmenaResolverModuleGraphModuleV0) -> &'static str {
355 if module.has_source_input && module.has_style_input && module.has_type_fact_input {
356 "ready"
357 } else if module.has_source_input && !module.has_style_input {
358 "sourceOnly"
359 } else if module.has_style_input && !module.has_source_input {
360 "styleOnly"
361 } else {
362 "partial"
363 }
364}
365
366pub fn summarize_omena_resolver_query_fragments(
367 input: &EngineInputV2,
368) -> SourceResolutionQueryFragmentsV0 {
369 summarize_source_resolution_query_fragments_input(input)
370}
371
372pub fn summarize_omena_resolver_canonical_producer_signal(
373 input: &EngineInputV2,
374) -> SourceResolutionCanonicalProducerSignalV0 {
375 summarize_source_resolution_canonical_producer_signal_input(input)
376}
377
378pub fn summarize_omena_resolver_source_resolution_runtime(
379 input: &EngineInputV2,
380) -> OmenaResolverSourceResolutionRuntimeIndexV0 {
381 let canonical_signal = summarize_omena_resolver_canonical_producer_signal(input);
382 let mut candidates_by_expression = BTreeMap::<String, SourceResolutionCandidateV0>::new();
383
384 for candidate in canonical_signal.canonical_bundle.candidates {
385 candidates_by_expression.insert(candidate.expression_id.clone(), candidate);
386 }
387
388 let entries = canonical_signal
389 .canonical_bundle
390 .query_fragments
391 .iter()
392 .map(|fragment| {
393 runtime_source_resolution_entry_from_fragment(
394 fragment,
395 candidates_by_expression.get(&fragment.expression_id),
396 )
397 })
398 .collect::<Vec<_>>();
399 let resolved_expression_count = entries
400 .iter()
401 .filter(|entry| entry.can_resolve_source_expression)
402 .count();
403 let unresolved_expression_count = entries.len() - resolved_expression_count;
404 let mut blocking_gaps = Vec::new();
405
406 if entries.is_empty() {
407 blocking_gaps.push("emptySourceResolutionRuntimeIndex");
408 }
409 if unresolved_expression_count > 0 {
410 blocking_gaps.push("unresolvedSourceExpressions");
411 }
412
413 OmenaResolverSourceResolutionRuntimeIndexV0 {
414 schema_version: "0",
415 product: "omena-resolver.source-resolution-runtime-index",
416 input_product: "engine-input-producers.source-resolution-canonical-producer",
417 input_version: canonical_signal.input_version,
418 expression_count: entries.len(),
419 resolved_expression_count,
420 unresolved_expression_count,
421 blocking_gaps,
422 entries,
423 }
424}
425
426pub fn query_omena_resolver_source_expression(
427 runtime_index: &OmenaResolverSourceResolutionRuntimeIndexV0,
428 expression_id: &str,
429) -> Option<OmenaResolverSourceResolutionRuntimeEntryV0> {
430 runtime_index
431 .entries
432 .iter()
433 .find(|entry| entry.expression_id == expression_id)
434 .cloned()
435}
436
437fn runtime_source_resolution_entry_from_fragment(
438 fragment: &SourceResolutionQueryFragmentV0,
439 candidate: Option<&SourceResolutionCandidateV0>,
440) -> OmenaResolverSourceResolutionRuntimeEntryV0 {
441 let selector_names = candidate
442 .map(|candidate| candidate.selector_names.clone())
443 .unwrap_or_default();
444 let finite_values = candidate.and_then(|candidate| candidate.finite_values.clone());
445 let has_selector_match = !selector_names.is_empty();
446 let has_finite_values = finite_values
447 .as_ref()
448 .is_some_and(|values| !values.is_empty());
449
450 OmenaResolverSourceResolutionRuntimeEntryV0 {
451 query_id: fragment.query_id.clone(),
452 expression_id: fragment.expression_id.clone(),
453 expression_kind: fragment.expression_kind.clone(),
454 style_file_path: fragment.style_file_path.clone(),
455 selector_names,
456 finite_values,
457 selector_certainty: candidate
458 .map(|candidate| candidate.selector_certainty.clone())
459 .unwrap_or_else(|| "unresolved".to_string()),
460 value_certainty: candidate.and_then(|candidate| candidate.value_certainty.clone()),
461 selector_certainty_shape_kind: candidate
462 .map(|candidate| candidate.selector_certainty_shape_kind.clone())
463 .unwrap_or_else(|| "missingTypeFacts".to_string()),
464 value_certainty_shape_kind: candidate
465 .map(|candidate| candidate.value_certainty_shape_kind.clone())
466 .unwrap_or_else(|| "missingTypeFacts".to_string()),
467 has_selector_match,
468 has_finite_values,
469 can_resolve_source_expression: has_selector_match,
470 status: if has_selector_match {
471 "resolved"
472 } else if candidate.is_some() {
473 "unresolvedSelectorSet"
474 } else {
475 "missingTypeFacts"
476 },
477 }
478}
479
480#[cfg(test)]
481mod tests {
482 use engine_input_producers::{
483 ClassExpressionInputV2, EngineInputV2, PositionV2, RangeV2, SourceAnalysisInputV2,
484 SourceDocumentV2, StringTypeFactsV2, StyleAnalysisInputV2, StyleDocumentV2,
485 StyleSelectorV2, TypeFactEntryV2,
486 };
487
488 use super::{
489 query_omena_resolver_runtime_module, query_omena_resolver_source_expression,
490 summarize_omena_resolver_boundary, summarize_omena_resolver_canonical_producer_signal,
491 summarize_omena_resolver_module_graph_index, summarize_omena_resolver_query_fragments,
492 summarize_omena_resolver_runtime_query_boundary,
493 summarize_omena_resolver_source_resolution_runtime,
494 };
495
496 #[test]
497 fn summarizes_resolver_boundary_over_source_resolution_products() {
498 let input = sample_input();
499 let summary = summarize_omena_resolver_boundary(&input);
500
501 assert_eq!(summary.schema_version, "0");
502 assert_eq!(summary.product, "omena-resolver.boundary");
503 assert_eq!(summary.resolver_name, "omena-resolver");
504 assert_eq!(summary.input_version, "2");
505 assert_eq!(summary.source_resolution_query_count, 2);
506 assert_eq!(summary.source_resolution_candidate_count, 2);
507 assert_eq!(summary.source_resolution_evaluator_candidate_count, 2);
508 assert_eq!(summary.module_graph_module_count, 2);
509 assert_eq!(summary.module_graph_source_expression_edge_count, 2);
510 assert_eq!(summary.runtime_query_module_count, 2);
511 assert_eq!(summary.runtime_query_ready_module_count, 2);
512 assert_eq!(summary.source_resolution_runtime_expression_count, 2);
513 assert_eq!(
514 summary.source_resolution_runtime_resolved_expression_count,
515 2
516 );
517 assert!(
518 summary
519 .delegated_source_resolution_products
520 .contains(&"engine-input-producers.source-resolution-canonical-producer")
521 );
522 assert!(
523 summary
524 .resolver_owned_products
525 .contains(&"omena-resolver.module-graph-index")
526 );
527 assert!(
528 summary
529 .resolver_owned_products
530 .contains(&"omena-resolver.runtime-query-boundary")
531 );
532 assert!(
533 summary
534 .resolver_owned_products
535 .contains(&"omena-resolver.source-resolution-runtime-index")
536 );
537 assert!(summary.ready_surfaces.contains(&"resolverModuleGraphIndex"));
538 assert!(
539 summary
540 .ready_surfaces
541 .contains(&"resolverRuntimeQueryBoundary")
542 );
543 assert!(
544 summary
545 .ready_surfaces
546 .contains(&"resolverSourceResolutionRuntimeIndex")
547 );
548 assert!(
549 summary
550 .next_decoupling_targets
551 .contains(&"tsconfigPathMapping")
552 );
553 }
554
555 #[test]
556 fn builds_resolver_module_graph_index_from_engine_input() {
557 let input = sample_input();
558 let summary = summarize_omena_resolver_module_graph_index(&input);
559
560 assert_eq!(summary.schema_version, "0");
561 assert_eq!(summary.product, "omena-resolver.module-graph-index");
562 assert_eq!(summary.input_version, "2");
563 assert_eq!(summary.module_count, 2);
564 assert_eq!(summary.source_expression_edge_count, 2);
565 assert_eq!(summary.type_fact_edge_count, 2);
566 assert_eq!(summary.selector_count, 2);
567 assert_eq!(summary.unresolved_type_fact_count, 0);
568 assert!(summary.unresolved_type_fact_expression_ids.is_empty());
569
570 let app = summary
571 .modules
572 .iter()
573 .find(|module| module.style_file_path == "/tmp/App.module.scss");
574 assert!(app.is_some());
575 let Some(app) = app else {
576 return;
577 };
578 assert_eq!(app.source_expression_ids, ["expr-1"]);
579 assert_eq!(app.source_expression_kinds, ["symbolRef"]);
580 assert_eq!(app.type_fact_expression_ids, ["expr-1"]);
581 assert_eq!(app.selector_names, ["btn-active"]);
582 assert_eq!(app.canonical_selector_names, ["btn-active"]);
583 assert!(app.has_source_input);
584 assert!(app.has_style_input);
585 assert!(app.has_type_fact_input);
586
587 let card = summary
588 .modules
589 .iter()
590 .find(|module| module.style_file_path == "/tmp/Card.module.scss");
591 assert!(card.is_some());
592 let Some(card) = card else {
593 return;
594 };
595 assert_eq!(card.source_expression_ids, ["expr-2"]);
596 assert_eq!(card.source_expression_kinds, ["styleAccess"]);
597 assert_eq!(card.type_fact_expression_ids, ["expr-2"]);
598 assert_eq!(card.selector_names, ["card-header"]);
599 assert_eq!(card.canonical_selector_names, ["card-header"]);
600 }
601
602 #[test]
603 fn exposes_runtime_query_boundary_from_module_graph_index() {
604 let input = sample_input();
605 let module_graph = summarize_omena_resolver_module_graph_index(&input);
606 let runtime_query = summarize_omena_resolver_runtime_query_boundary(&module_graph);
607
608 assert_eq!(runtime_query.schema_version, "0");
609 assert_eq!(
610 runtime_query.product,
611 "omena-resolver.runtime-query-boundary"
612 );
613 assert_eq!(
614 runtime_query.input_product,
615 "omena-resolver.module-graph-index"
616 );
617 assert_eq!(runtime_query.input_version, "2");
618 assert_eq!(runtime_query.module_query_count, 2);
619 assert_eq!(runtime_query.fully_resolvable_module_count, 2);
620 assert_eq!(runtime_query.source_only_module_count, 0);
621 assert_eq!(runtime_query.style_only_module_count, 0);
622 assert_eq!(runtime_query.unresolved_type_fact_count, 0);
623 assert!(runtime_query.blocking_gaps.is_empty());
624 assert!(
625 runtime_query
626 .runtime_capabilities
627 .contains(&"moduleLookupByStylePath")
628 );
629
630 let app = query_omena_resolver_runtime_module(&module_graph, "/tmp/App.module.scss");
631 assert!(app.is_some());
632 let Some(app) = app else {
633 return;
634 };
635 assert_eq!(app.status, "ready");
636 assert!(app.can_resolve_source_expressions);
637 assert!(app.can_check_type_fact_edges);
638 assert!(app.can_query_selector_names);
639 assert_eq!(app.source_expression_ids, ["expr-1"]);
640 assert_eq!(app.selector_names, ["btn-active"]);
641 }
642
643 #[test]
644 fn builds_source_resolution_runtime_index_from_canonical_candidates() {
645 let input = sample_input();
646 let runtime_index = summarize_omena_resolver_source_resolution_runtime(&input);
647
648 assert_eq!(runtime_index.schema_version, "0");
649 assert_eq!(
650 runtime_index.product,
651 "omena-resolver.source-resolution-runtime-index"
652 );
653 assert_eq!(
654 runtime_index.input_product,
655 "engine-input-producers.source-resolution-canonical-producer"
656 );
657 assert_eq!(runtime_index.input_version, "2");
658 assert_eq!(runtime_index.expression_count, 2);
659 assert_eq!(runtime_index.resolved_expression_count, 2);
660 assert_eq!(runtime_index.unresolved_expression_count, 0);
661 assert!(runtime_index.blocking_gaps.is_empty());
662
663 let app = query_omena_resolver_source_expression(&runtime_index, "expr-1");
664 assert!(app.is_some());
665 let Some(app) = app else {
666 return;
667 };
668 assert_eq!(app.query_id, "expr-1");
669 assert_eq!(app.expression_kind, "symbolRef");
670 assert_eq!(app.style_file_path, "/tmp/App.module.scss");
671 assert_eq!(app.selector_names, ["btn-active"]);
672 assert_eq!(app.selector_certainty, "exact");
673 assert_eq!(app.selector_certainty_shape_kind, "exact");
674 assert_eq!(app.value_certainty_shape_kind, "constrained");
675 assert!(app.has_selector_match);
676 assert!(!app.has_finite_values);
677 assert!(app.can_resolve_source_expression);
678 assert_eq!(app.status, "resolved");
679
680 let card = query_omena_resolver_source_expression(&runtime_index, "expr-2");
681 assert!(card.is_some());
682 let Some(card) = card else {
683 return;
684 };
685 assert_eq!(card.selector_names, ["card-header"]);
686 assert_eq!(
687 card.finite_values,
688 Some(vec!["card-header".to_string(), "card-body".to_string()])
689 );
690 assert!(card.has_finite_values);
691 }
692
693 #[test]
694 fn exposes_stable_query_fragment_and_canonical_producer_wrappers() {
695 let input = sample_input();
696
697 let query_fragments = summarize_omena_resolver_query_fragments(&input);
698 assert_eq!(query_fragments.schema_version, "0");
699 assert_eq!(query_fragments.input_version, "2");
700 assert_eq!(query_fragments.fragments.len(), 2);
701 assert_eq!(query_fragments.fragments[0].query_id, "expr-1");
702 assert_eq!(
703 query_fragments.fragments[1].style_file_path,
704 "/tmp/Card.module.scss"
705 );
706
707 let canonical_signal = summarize_omena_resolver_canonical_producer_signal(&input);
708 assert_eq!(canonical_signal.schema_version, "0");
709 assert_eq!(canonical_signal.input_version, "2");
710 assert_eq!(canonical_signal.canonical_bundle.query_fragments.len(), 2);
711 assert_eq!(canonical_signal.canonical_bundle.candidates.len(), 2);
712 assert_eq!(canonical_signal.evaluator_candidates.results.len(), 2);
713 }
714
715 fn sample_input() -> EngineInputV2 {
716 EngineInputV2 {
717 version: "2".to_string(),
718 sources: vec![SourceAnalysisInputV2 {
719 document: SourceDocumentV2 {
720 class_expressions: vec![
721 ClassExpressionInputV2 {
722 id: "expr-1".to_string(),
723 kind: "symbolRef".to_string(),
724 scss_module_path: "/tmp/App.module.scss".to_string(),
725 range: range(4, 12, 4, 16),
726 class_name: None,
727 root_binding_decl_id: Some("decl-1".to_string()),
728 access_path: None,
729 },
730 ClassExpressionInputV2 {
731 id: "expr-2".to_string(),
732 kind: "styleAccess".to_string(),
733 scss_module_path: "/tmp/Card.module.scss".to_string(),
734 range: range(6, 9, 6, 20),
735 class_name: Some("card-header".to_string()),
736 root_binding_decl_id: None,
737 access_path: Some(vec!["card".to_string(), "header".to_string()]),
738 },
739 ],
740 },
741 }],
742 styles: vec![
743 StyleAnalysisInputV2 {
744 file_path: "/tmp/App.module.scss".to_string(),
745 document: StyleDocumentV2 {
746 selectors: vec![StyleSelectorV2 {
747 name: "btn-active".to_string(),
748 view_kind: "canonical".to_string(),
749 canonical_name: Some("btn-active".to_string()),
750 range: range(1, 1, 1, 12),
751 nested_safety: Some("safe".to_string()),
752 composes: None,
753 bem_suffix: None,
754 }],
755 },
756 },
757 StyleAnalysisInputV2 {
758 file_path: "/tmp/Card.module.scss".to_string(),
759 document: StyleDocumentV2 {
760 selectors: vec![StyleSelectorV2 {
761 name: "card-header".to_string(),
762 view_kind: "canonical".to_string(),
763 canonical_name: Some("card-header".to_string()),
764 range: range(3, 1, 3, 13),
765 nested_safety: Some("unsafe".to_string()),
766 composes: None,
767 bem_suffix: None,
768 }],
769 },
770 },
771 ],
772 type_facts: vec![
773 TypeFactEntryV2 {
774 file_path: "/tmp/App.tsx".to_string(),
775 expression_id: "expr-1".to_string(),
776 facts: StringTypeFactsV2 {
777 kind: "constrained".to_string(),
778 constraint_kind: Some("prefixSuffix".to_string()),
779 values: None,
780 prefix: Some("btn-".to_string()),
781 suffix: Some("-active".to_string()),
782 min_len: Some(10),
783 max_len: None,
784 char_must: None,
785 char_may: None,
786 may_include_other_chars: None,
787 },
788 },
789 TypeFactEntryV2 {
790 file_path: "/tmp/Card.tsx".to_string(),
791 expression_id: "expr-2".to_string(),
792 facts: StringTypeFactsV2 {
793 kind: "finiteSet".to_string(),
794 constraint_kind: None,
795 values: Some(vec!["card-header".to_string(), "card-body".to_string()]),
796 prefix: None,
797 suffix: None,
798 min_len: None,
799 max_len: None,
800 char_must: None,
801 char_may: None,
802 may_include_other_chars: None,
803 },
804 },
805 ],
806 }
807 }
808
809 fn range(
810 start_line: usize,
811 start_character: usize,
812 end_line: usize,
813 end_character: usize,
814 ) -> RangeV2 {
815 RangeV2 {
816 start: PositionV2 {
817 line: start_line,
818 character: start_character,
819 },
820 end: PositionV2 {
821 line: end_line,
822 character: end_character,
823 },
824 }
825 }
826}