1use std::collections::{BTreeMap, BTreeSet};
2
3use engine_input_producers::{
4 EngineInputV2, SourceResolutionCanonicalProducerSignalV0, SourceResolutionQueryFragmentsV0,
5 summarize_source_resolution_canonical_producer_signal_input,
6 summarize_source_resolution_query_fragments_input,
7};
8use serde::{Deserialize, Serialize};
9
10#[derive(Debug, Serialize)]
11#[serde(rename_all = "camelCase")]
12pub struct OmenaResolverBoundarySummaryV0 {
13 pub schema_version: &'static str,
14 pub product: &'static str,
15 pub resolver_name: &'static str,
16 pub input_version: String,
17 pub delegated_source_resolution_products: Vec<&'static str>,
18 pub resolver_owned_products: Vec<&'static str>,
19 pub source_resolution_query_count: usize,
20 pub source_resolution_candidate_count: usize,
21 pub source_resolution_evaluator_candidate_count: usize,
22 pub module_graph_module_count: usize,
23 pub module_graph_source_expression_edge_count: usize,
24 pub runtime_query_module_count: usize,
25 pub runtime_query_ready_module_count: usize,
26 pub ready_surfaces: Vec<&'static str>,
27 pub cme_coupled_surfaces: Vec<&'static str>,
28 pub next_decoupling_targets: Vec<&'static str>,
29}
30
31#[derive(Debug, Clone, PartialEq, Eq, Deserialize, Serialize)]
32#[serde(rename_all = "camelCase")]
33pub struct OmenaResolverModuleGraphSummaryV0 {
34 pub schema_version: String,
35 pub product: String,
36 pub input_version: String,
37 pub module_count: usize,
38 pub source_expression_edge_count: usize,
39 pub type_fact_edge_count: usize,
40 pub selector_count: usize,
41 pub unresolved_type_fact_count: usize,
42 pub modules: Vec<OmenaResolverModuleGraphModuleV0>,
43 pub unresolved_type_fact_expression_ids: Vec<String>,
44}
45
46#[derive(Debug, Clone, PartialEq, Eq, Deserialize, Serialize)]
47#[serde(rename_all = "camelCase")]
48pub struct OmenaResolverModuleGraphModuleV0 {
49 pub style_file_path: String,
50 pub source_expression_ids: Vec<String>,
51 pub source_expression_kinds: Vec<String>,
52 pub type_fact_expression_ids: Vec<String>,
53 pub selector_names: Vec<String>,
54 pub canonical_selector_names: Vec<String>,
55 pub has_source_input: bool,
56 pub has_style_input: bool,
57 pub has_type_fact_input: bool,
58}
59
60#[derive(Debug, Clone, PartialEq, Eq, Serialize)]
61#[serde(rename_all = "camelCase")]
62pub struct OmenaResolverRuntimeQueryBoundarySummaryV0 {
63 pub schema_version: &'static str,
64 pub product: &'static str,
65 pub input_product: String,
66 pub input_version: String,
67 pub module_query_count: usize,
68 pub fully_resolvable_module_count: usize,
69 pub source_only_module_count: usize,
70 pub style_only_module_count: usize,
71 pub unresolved_type_fact_count: usize,
72 pub runtime_capabilities: Vec<&'static str>,
73 pub blocking_gaps: Vec<&'static str>,
74 pub module_queries: Vec<OmenaResolverRuntimeModuleQueryV0>,
75}
76
77#[derive(Debug, Clone, PartialEq, Eq, Serialize)]
78#[serde(rename_all = "camelCase")]
79pub struct OmenaResolverRuntimeModuleQueryV0 {
80 pub style_file_path: String,
81 pub source_expression_ids: Vec<String>,
82 pub type_fact_expression_ids: Vec<String>,
83 pub selector_names: Vec<String>,
84 pub canonical_selector_names: Vec<String>,
85 pub can_resolve_source_expressions: bool,
86 pub can_check_type_fact_edges: bool,
87 pub can_query_selector_names: bool,
88 pub status: &'static str,
89}
90
91#[derive(Debug, Default)]
92struct ModuleGraphAccumulator {
93 source_expression_ids: BTreeSet<String>,
94 source_expression_kinds: BTreeSet<String>,
95 type_fact_expression_ids: BTreeSet<String>,
96 selector_names: BTreeSet<String>,
97 canonical_selector_names: BTreeSet<String>,
98 has_source_input: bool,
99 has_style_input: bool,
100 has_type_fact_input: bool,
101}
102
103pub fn summarize_omena_resolver_boundary(input: &EngineInputV2) -> OmenaResolverBoundarySummaryV0 {
104 let canonical_signal = summarize_omena_resolver_canonical_producer_signal(input);
105 let module_graph = summarize_omena_resolver_module_graph_index(input);
106 let runtime_query = summarize_omena_resolver_runtime_query_boundary(&module_graph);
107
108 OmenaResolverBoundarySummaryV0 {
109 schema_version: "0",
110 product: "omena-resolver.boundary",
111 resolver_name: "omena-resolver",
112 input_version: input.version.clone(),
113 delegated_source_resolution_products: vec![
114 "engine-input-producers.source-resolution-query-fragments",
115 "engine-input-producers.source-resolution-canonical-producer",
116 ],
117 resolver_owned_products: vec![
118 "omena-resolver.module-graph-index",
119 "omena-resolver.runtime-query-boundary",
120 ],
121 source_resolution_query_count: canonical_signal.canonical_bundle.query_fragments.len(),
122 source_resolution_candidate_count: canonical_signal.canonical_bundle.candidates.len(),
123 source_resolution_evaluator_candidate_count: canonical_signal
124 .evaluator_candidates
125 .results
126 .len(),
127 module_graph_module_count: module_graph.module_count,
128 module_graph_source_expression_edge_count: module_graph.source_expression_edge_count,
129 runtime_query_module_count: runtime_query.module_query_count,
130 runtime_query_ready_module_count: runtime_query.fully_resolvable_module_count,
131 ready_surfaces: vec![
132 "resolverBoundarySummary",
133 "resolverModuleGraphIndex",
134 "resolverRuntimeQueryBoundary",
135 "sourceResolutionQueryFragments",
136 "sourceResolutionCanonicalProducerSignal",
137 ],
138 cme_coupled_surfaces: vec!["EngineInputV2", "producerSourceResolutionRows"],
139 next_decoupling_targets: vec!["specifierResolutionRuntime", "tsconfigPathMapping"],
140 }
141}
142
143pub fn summarize_omena_resolver_module_graph_index(
144 input: &EngineInputV2,
145) -> OmenaResolverModuleGraphSummaryV0 {
146 let mut modules = BTreeMap::<String, ModuleGraphAccumulator>::new();
147 let mut expression_to_style_path = BTreeMap::<String, String>::new();
148 let mut source_expression_edge_count = 0usize;
149 let mut type_fact_edge_count = 0usize;
150 let mut selector_count = 0usize;
151 let mut unresolved_type_fact_expression_ids = BTreeSet::<String>::new();
152
153 for source in &input.sources {
154 for expression in &source.document.class_expressions {
155 source_expression_edge_count += 1;
156 expression_to_style_path
157 .insert(expression.id.clone(), expression.scss_module_path.clone());
158 let module = modules
159 .entry(expression.scss_module_path.clone())
160 .or_default();
161 module.has_source_input = true;
162 module.source_expression_ids.insert(expression.id.clone());
163 module
164 .source_expression_kinds
165 .insert(expression.kind.clone());
166 }
167 }
168
169 for style in &input.styles {
170 let module = modules.entry(style.file_path.clone()).or_default();
171 module.has_style_input = true;
172 for selector in &style.document.selectors {
173 selector_count += 1;
174 module.selector_names.insert(selector.name.clone());
175 if let Some(canonical_name) = &selector.canonical_name {
176 module
177 .canonical_selector_names
178 .insert(canonical_name.clone());
179 }
180 }
181 }
182
183 for type_fact in &input.type_facts {
184 if let Some(style_file_path) = expression_to_style_path.get(&type_fact.expression_id) {
185 type_fact_edge_count += 1;
186 let module = modules.entry(style_file_path.clone()).or_default();
187 module.has_type_fact_input = true;
188 module
189 .type_fact_expression_ids
190 .insert(type_fact.expression_id.clone());
191 } else {
192 unresolved_type_fact_expression_ids.insert(type_fact.expression_id.clone());
193 }
194 }
195
196 let modules = modules
197 .into_iter()
198 .map(
199 |(style_file_path, module)| OmenaResolverModuleGraphModuleV0 {
200 style_file_path,
201 source_expression_ids: module.source_expression_ids.into_iter().collect(),
202 source_expression_kinds: module.source_expression_kinds.into_iter().collect(),
203 type_fact_expression_ids: module.type_fact_expression_ids.into_iter().collect(),
204 selector_names: module.selector_names.into_iter().collect(),
205 canonical_selector_names: module.canonical_selector_names.into_iter().collect(),
206 has_source_input: module.has_source_input,
207 has_style_input: module.has_style_input,
208 has_type_fact_input: module.has_type_fact_input,
209 },
210 )
211 .collect::<Vec<_>>();
212 let unresolved_type_fact_expression_ids = unresolved_type_fact_expression_ids
213 .into_iter()
214 .collect::<Vec<_>>();
215
216 OmenaResolverModuleGraphSummaryV0 {
217 schema_version: "0".to_string(),
218 product: "omena-resolver.module-graph-index".to_string(),
219 input_version: input.version.clone(),
220 module_count: modules.len(),
221 source_expression_edge_count,
222 type_fact_edge_count,
223 selector_count,
224 unresolved_type_fact_count: unresolved_type_fact_expression_ids.len(),
225 modules,
226 unresolved_type_fact_expression_ids,
227 }
228}
229
230pub fn summarize_omena_resolver_runtime_query_boundary(
231 module_graph: &OmenaResolverModuleGraphSummaryV0,
232) -> OmenaResolverRuntimeQueryBoundarySummaryV0 {
233 let module_queries = module_graph
234 .modules
235 .iter()
236 .map(runtime_module_query_from_graph_module)
237 .collect::<Vec<_>>();
238 let fully_resolvable_module_count = module_queries
239 .iter()
240 .filter(|module| module.status == "ready")
241 .count();
242 let source_only_module_count = module_graph
243 .modules
244 .iter()
245 .filter(|module| module.has_source_input && !module.has_style_input)
246 .count();
247 let style_only_module_count = module_graph
248 .modules
249 .iter()
250 .filter(|module| module.has_style_input && !module.has_source_input)
251 .count();
252 let mut blocking_gaps = Vec::new();
253
254 if module_graph.module_count == 0 {
255 blocking_gaps.push("emptyModuleGraph");
256 }
257 if fully_resolvable_module_count < module_graph.module_count {
258 blocking_gaps.push("partialModuleCoverage");
259 }
260 if module_graph.unresolved_type_fact_count > 0 {
261 blocking_gaps.push("unresolvedTypeFactEdges");
262 }
263
264 OmenaResolverRuntimeQueryBoundarySummaryV0 {
265 schema_version: "0",
266 product: "omena-resolver.runtime-query-boundary",
267 input_product: module_graph.product.clone(),
268 input_version: module_graph.input_version.clone(),
269 module_query_count: module_queries.len(),
270 fully_resolvable_module_count,
271 source_only_module_count,
272 style_only_module_count,
273 unresolved_type_fact_count: module_graph.unresolved_type_fact_count,
274 runtime_capabilities: vec![
275 "moduleLookupByStylePath",
276 "sourceExpressionEdgeLookup",
277 "typeFactEdgeLookup",
278 "selectorNameLookup",
279 ],
280 blocking_gaps,
281 module_queries,
282 }
283}
284
285pub fn query_omena_resolver_runtime_module(
286 module_graph: &OmenaResolverModuleGraphSummaryV0,
287 style_file_path: &str,
288) -> Option<OmenaResolverRuntimeModuleQueryV0> {
289 module_graph
290 .modules
291 .iter()
292 .find(|module| module.style_file_path == style_file_path)
293 .map(runtime_module_query_from_graph_module)
294}
295
296fn runtime_module_query_from_graph_module(
297 module: &OmenaResolverModuleGraphModuleV0,
298) -> OmenaResolverRuntimeModuleQueryV0 {
299 OmenaResolverRuntimeModuleQueryV0 {
300 style_file_path: module.style_file_path.clone(),
301 source_expression_ids: module.source_expression_ids.clone(),
302 type_fact_expression_ids: module.type_fact_expression_ids.clone(),
303 selector_names: module.selector_names.clone(),
304 canonical_selector_names: module.canonical_selector_names.clone(),
305 can_resolve_source_expressions: module.has_source_input && module.has_style_input,
306 can_check_type_fact_edges: module.has_source_input && module.has_type_fact_input,
307 can_query_selector_names: module.has_style_input,
308 status: module_runtime_status(module),
309 }
310}
311
312fn module_runtime_status(module: &OmenaResolverModuleGraphModuleV0) -> &'static str {
313 if module.has_source_input && module.has_style_input && module.has_type_fact_input {
314 "ready"
315 } else if module.has_source_input && !module.has_style_input {
316 "sourceOnly"
317 } else if module.has_style_input && !module.has_source_input {
318 "styleOnly"
319 } else {
320 "partial"
321 }
322}
323
324pub fn summarize_omena_resolver_query_fragments(
325 input: &EngineInputV2,
326) -> SourceResolutionQueryFragmentsV0 {
327 summarize_source_resolution_query_fragments_input(input)
328}
329
330pub fn summarize_omena_resolver_canonical_producer_signal(
331 input: &EngineInputV2,
332) -> SourceResolutionCanonicalProducerSignalV0 {
333 summarize_source_resolution_canonical_producer_signal_input(input)
334}
335
336#[cfg(test)]
337mod tests {
338 use engine_input_producers::{
339 ClassExpressionInputV2, EngineInputV2, PositionV2, RangeV2, SourceAnalysisInputV2,
340 SourceDocumentV2, StringTypeFactsV2, StyleAnalysisInputV2, StyleDocumentV2,
341 StyleSelectorV2, TypeFactEntryV2,
342 };
343
344 use super::{
345 query_omena_resolver_runtime_module, summarize_omena_resolver_boundary,
346 summarize_omena_resolver_canonical_producer_signal,
347 summarize_omena_resolver_module_graph_index, summarize_omena_resolver_query_fragments,
348 summarize_omena_resolver_runtime_query_boundary,
349 };
350
351 #[test]
352 fn summarizes_resolver_boundary_over_source_resolution_products() {
353 let input = sample_input();
354 let summary = summarize_omena_resolver_boundary(&input);
355
356 assert_eq!(summary.schema_version, "0");
357 assert_eq!(summary.product, "omena-resolver.boundary");
358 assert_eq!(summary.resolver_name, "omena-resolver");
359 assert_eq!(summary.input_version, "2");
360 assert_eq!(summary.source_resolution_query_count, 2);
361 assert_eq!(summary.source_resolution_candidate_count, 2);
362 assert_eq!(summary.source_resolution_evaluator_candidate_count, 2);
363 assert_eq!(summary.module_graph_module_count, 2);
364 assert_eq!(summary.module_graph_source_expression_edge_count, 2);
365 assert_eq!(summary.runtime_query_module_count, 2);
366 assert_eq!(summary.runtime_query_ready_module_count, 2);
367 assert!(
368 summary
369 .delegated_source_resolution_products
370 .contains(&"engine-input-producers.source-resolution-canonical-producer")
371 );
372 assert!(
373 summary
374 .resolver_owned_products
375 .contains(&"omena-resolver.module-graph-index")
376 );
377 assert!(
378 summary
379 .resolver_owned_products
380 .contains(&"omena-resolver.runtime-query-boundary")
381 );
382 assert!(summary.ready_surfaces.contains(&"resolverModuleGraphIndex"));
383 assert!(
384 summary
385 .ready_surfaces
386 .contains(&"resolverRuntimeQueryBoundary")
387 );
388 assert!(
389 summary
390 .next_decoupling_targets
391 .contains(&"tsconfigPathMapping")
392 );
393 }
394
395 #[test]
396 fn builds_resolver_module_graph_index_from_engine_input() {
397 let input = sample_input();
398 let summary = summarize_omena_resolver_module_graph_index(&input);
399
400 assert_eq!(summary.schema_version, "0");
401 assert_eq!(summary.product, "omena-resolver.module-graph-index");
402 assert_eq!(summary.input_version, "2");
403 assert_eq!(summary.module_count, 2);
404 assert_eq!(summary.source_expression_edge_count, 2);
405 assert_eq!(summary.type_fact_edge_count, 2);
406 assert_eq!(summary.selector_count, 2);
407 assert_eq!(summary.unresolved_type_fact_count, 0);
408 assert!(summary.unresolved_type_fact_expression_ids.is_empty());
409
410 let app = summary
411 .modules
412 .iter()
413 .find(|module| module.style_file_path == "/tmp/App.module.scss");
414 assert!(app.is_some());
415 let Some(app) = app else {
416 return;
417 };
418 assert_eq!(app.source_expression_ids, ["expr-1"]);
419 assert_eq!(app.source_expression_kinds, ["symbolRef"]);
420 assert_eq!(app.type_fact_expression_ids, ["expr-1"]);
421 assert_eq!(app.selector_names, ["btn-active"]);
422 assert_eq!(app.canonical_selector_names, ["btn-active"]);
423 assert!(app.has_source_input);
424 assert!(app.has_style_input);
425 assert!(app.has_type_fact_input);
426
427 let card = summary
428 .modules
429 .iter()
430 .find(|module| module.style_file_path == "/tmp/Card.module.scss");
431 assert!(card.is_some());
432 let Some(card) = card else {
433 return;
434 };
435 assert_eq!(card.source_expression_ids, ["expr-2"]);
436 assert_eq!(card.source_expression_kinds, ["styleAccess"]);
437 assert_eq!(card.type_fact_expression_ids, ["expr-2"]);
438 assert_eq!(card.selector_names, ["card-header"]);
439 assert_eq!(card.canonical_selector_names, ["card-header"]);
440 }
441
442 #[test]
443 fn exposes_runtime_query_boundary_from_module_graph_index() {
444 let input = sample_input();
445 let module_graph = summarize_omena_resolver_module_graph_index(&input);
446 let runtime_query = summarize_omena_resolver_runtime_query_boundary(&module_graph);
447
448 assert_eq!(runtime_query.schema_version, "0");
449 assert_eq!(
450 runtime_query.product,
451 "omena-resolver.runtime-query-boundary"
452 );
453 assert_eq!(
454 runtime_query.input_product,
455 "omena-resolver.module-graph-index"
456 );
457 assert_eq!(runtime_query.input_version, "2");
458 assert_eq!(runtime_query.module_query_count, 2);
459 assert_eq!(runtime_query.fully_resolvable_module_count, 2);
460 assert_eq!(runtime_query.source_only_module_count, 0);
461 assert_eq!(runtime_query.style_only_module_count, 0);
462 assert_eq!(runtime_query.unresolved_type_fact_count, 0);
463 assert!(runtime_query.blocking_gaps.is_empty());
464 assert!(
465 runtime_query
466 .runtime_capabilities
467 .contains(&"moduleLookupByStylePath")
468 );
469
470 let app = query_omena_resolver_runtime_module(&module_graph, "/tmp/App.module.scss");
471 assert!(app.is_some());
472 let Some(app) = app else {
473 return;
474 };
475 assert_eq!(app.status, "ready");
476 assert!(app.can_resolve_source_expressions);
477 assert!(app.can_check_type_fact_edges);
478 assert!(app.can_query_selector_names);
479 assert_eq!(app.source_expression_ids, ["expr-1"]);
480 assert_eq!(app.selector_names, ["btn-active"]);
481 }
482
483 #[test]
484 fn exposes_stable_query_fragment_and_canonical_producer_wrappers() {
485 let input = sample_input();
486
487 let query_fragments = summarize_omena_resolver_query_fragments(&input);
488 assert_eq!(query_fragments.schema_version, "0");
489 assert_eq!(query_fragments.input_version, "2");
490 assert_eq!(query_fragments.fragments.len(), 2);
491 assert_eq!(query_fragments.fragments[0].query_id, "expr-1");
492 assert_eq!(
493 query_fragments.fragments[1].style_file_path,
494 "/tmp/Card.module.scss"
495 );
496
497 let canonical_signal = summarize_omena_resolver_canonical_producer_signal(&input);
498 assert_eq!(canonical_signal.schema_version, "0");
499 assert_eq!(canonical_signal.input_version, "2");
500 assert_eq!(canonical_signal.canonical_bundle.query_fragments.len(), 2);
501 assert_eq!(canonical_signal.canonical_bundle.candidates.len(), 2);
502 assert_eq!(canonical_signal.evaluator_candidates.results.len(), 2);
503 }
504
505 fn sample_input() -> EngineInputV2 {
506 EngineInputV2 {
507 version: "2".to_string(),
508 sources: vec![SourceAnalysisInputV2 {
509 document: SourceDocumentV2 {
510 class_expressions: vec![
511 ClassExpressionInputV2 {
512 id: "expr-1".to_string(),
513 kind: "symbolRef".to_string(),
514 scss_module_path: "/tmp/App.module.scss".to_string(),
515 range: range(4, 12, 4, 16),
516 class_name: None,
517 root_binding_decl_id: Some("decl-1".to_string()),
518 access_path: None,
519 },
520 ClassExpressionInputV2 {
521 id: "expr-2".to_string(),
522 kind: "styleAccess".to_string(),
523 scss_module_path: "/tmp/Card.module.scss".to_string(),
524 range: range(6, 9, 6, 20),
525 class_name: Some("card-header".to_string()),
526 root_binding_decl_id: None,
527 access_path: Some(vec!["card".to_string(), "header".to_string()]),
528 },
529 ],
530 },
531 }],
532 styles: vec![
533 StyleAnalysisInputV2 {
534 file_path: "/tmp/App.module.scss".to_string(),
535 document: StyleDocumentV2 {
536 selectors: vec![StyleSelectorV2 {
537 name: "btn-active".to_string(),
538 view_kind: "canonical".to_string(),
539 canonical_name: Some("btn-active".to_string()),
540 range: range(1, 1, 1, 12),
541 nested_safety: Some("safe".to_string()),
542 composes: None,
543 bem_suffix: None,
544 }],
545 },
546 },
547 StyleAnalysisInputV2 {
548 file_path: "/tmp/Card.module.scss".to_string(),
549 document: StyleDocumentV2 {
550 selectors: vec![StyleSelectorV2 {
551 name: "card-header".to_string(),
552 view_kind: "canonical".to_string(),
553 canonical_name: Some("card-header".to_string()),
554 range: range(3, 1, 3, 13),
555 nested_safety: Some("unsafe".to_string()),
556 composes: None,
557 bem_suffix: None,
558 }],
559 },
560 },
561 ],
562 type_facts: vec![
563 TypeFactEntryV2 {
564 file_path: "/tmp/App.tsx".to_string(),
565 expression_id: "expr-1".to_string(),
566 facts: StringTypeFactsV2 {
567 kind: "constrained".to_string(),
568 constraint_kind: Some("prefixSuffix".to_string()),
569 values: None,
570 prefix: Some("btn-".to_string()),
571 suffix: Some("-active".to_string()),
572 min_len: Some(10),
573 max_len: None,
574 char_must: None,
575 char_may: None,
576 may_include_other_chars: None,
577 },
578 },
579 TypeFactEntryV2 {
580 file_path: "/tmp/Card.tsx".to_string(),
581 expression_id: "expr-2".to_string(),
582 facts: StringTypeFactsV2 {
583 kind: "finiteSet".to_string(),
584 constraint_kind: None,
585 values: Some(vec!["card-header".to_string(), "card-body".to_string()]),
586 prefix: None,
587 suffix: None,
588 min_len: None,
589 max_len: None,
590 char_must: None,
591 char_may: None,
592 may_include_other_chars: None,
593 },
594 },
595 ],
596 }
597 }
598
599 fn range(
600 start_line: usize,
601 start_character: usize,
602 end_line: usize,
603 end_character: usize,
604 ) -> RangeV2 {
605 RangeV2 {
606 start: PositionV2 {
607 line: start_line,
608 character: start_character,
609 },
610 end: PositionV2 {
611 line: end_line,
612 character: end_character,
613 },
614 }
615 }
616}