1use engine_input_producers::{
2 EngineInputV2, ExpressionDomainFlowAnalysisV0, ExpressionSemanticsCanonicalProducerSignalV0,
3 ExpressionSemanticsQueryFragmentsV0, SelectorUsageCanonicalProducerSignalV0,
4 SelectorUsageQueryFragmentsV0, SourceResolutionCanonicalProducerSignalV0,
5 SourceResolutionQueryFragmentsV0, summarize_expression_domain_flow_analysis_input,
6 summarize_expression_semantics_canonical_producer_signal_input,
7 summarize_expression_semantics_query_fragments_input,
8 summarize_selector_usage_canonical_producer_signal_input,
9 summarize_selector_usage_query_fragments_input,
10};
11use std::collections::{BTreeMap, BTreeSet, VecDeque};
12use std::path::{Component, Path, PathBuf};
13
14use engine_style_parser::{Stylesheet, parse_style_module, summarize_css_modules_intermediate};
15use omena_abstract_value::{AbstractValueDomainSummaryV0, summarize_omena_abstract_value_domain};
16use omena_bridge::{
17 DesignTokenExternalDeclarationCandidateScopeV0, DesignTokenWorkspaceDeclarationFactV0,
18 StyleSemanticGraphSummaryV0, collect_omena_bridge_design_token_workspace_declarations,
19 summarize_omena_bridge_style_semantic_graph_for_path_with_scoped_workspace_declarations,
20 summarize_omena_bridge_style_semantic_graph_from_source,
21};
22use omena_resolver::{
23 summarize_omena_resolver_canonical_producer_signal, summarize_omena_resolver_query_fragments,
24};
25use serde::Serialize;
26
27#[derive(Debug, Serialize)]
28#[serde(rename_all = "camelCase")]
29pub struct OmenaQueryBoundarySummaryV0 {
30 pub schema_version: &'static str,
31 pub product: &'static str,
32 pub query_engine_name: &'static str,
33 pub input_version: String,
34 pub abstract_value_domain: AbstractValueDomainSummaryV0,
35 pub selected_query_adapter_capabilities: SelectedQueryAdapterCapabilitiesV0,
36 pub delegated_fragment_products: Vec<&'static str>,
37 pub expression_semantics_query_count: usize,
38 pub source_resolution_query_count: usize,
39 pub selector_usage_query_count: usize,
40 pub total_query_count: usize,
41 pub ready_surfaces: Vec<&'static str>,
42 pub cme_coupled_surfaces: Vec<&'static str>,
43 pub next_decoupling_targets: Vec<&'static str>,
44}
45
46#[derive(Debug, Serialize)]
47#[serde(rename_all = "camelCase")]
48pub struct OmenaQueryFragmentBundleV0 {
49 pub schema_version: &'static str,
50 pub product: &'static str,
51 pub input_version: String,
52 pub expression_semantics: ExpressionSemanticsQueryFragmentsV0,
53 pub source_resolution: SourceResolutionQueryFragmentsV0,
54 pub selector_usage: SelectorUsageQueryFragmentsV0,
55}
56
57#[derive(Debug, Clone, PartialEq, Eq, Serialize)]
58#[serde(rename_all = "camelCase")]
59pub struct SelectedQueryAdapterCapabilitiesV0 {
60 pub schema_version: &'static str,
61 pub product: &'static str,
62 pub default_candidate_backend: &'static str,
63 pub backend_kinds: Vec<SelectedQueryBackendCapabilityV0>,
64 pub runner_commands: Vec<SelectedQueryRunnerCommandV0>,
65 pub expression_semantics_payload_contracts: Vec<&'static str>,
66 pub required_input_contracts: Vec<&'static str>,
67 pub adapter_readiness: Vec<&'static str>,
68 pub routing_status: &'static str,
69}
70
71#[derive(Debug, Clone, PartialEq, Eq, Serialize)]
72#[serde(rename_all = "camelCase")]
73pub struct SelectedQueryBackendCapabilityV0 {
74 pub backend_kind: &'static str,
75 pub source_resolution: bool,
76 pub expression_semantics: bool,
77 pub selector_usage: bool,
78 pub style_semantic_graph: bool,
79}
80
81#[derive(Debug, Clone, PartialEq, Eq, Serialize)]
82#[serde(rename_all = "camelCase")]
83pub struct SelectedQueryRunnerCommandV0 {
84 pub surface: &'static str,
85 pub command: &'static str,
86 pub input_contract: &'static str,
87 pub output_product: &'static str,
88}
89
90#[derive(Debug, Serialize)]
91#[serde(rename_all = "camelCase")]
92pub struct OmenaQueryStyleSemanticGraphBatchOutputV0 {
93 pub schema_version: &'static str,
94 pub product: &'static str,
95 pub graphs: Vec<OmenaQueryStyleSemanticGraphBatchEntryV0>,
96}
97
98#[derive(Debug, Serialize)]
99#[serde(rename_all = "camelCase")]
100pub struct OmenaQueryStyleSemanticGraphBatchEntryV0 {
101 pub style_path: String,
102 pub graph: Option<StyleSemanticGraphSummaryV0>,
103}
104
105pub fn summarize_omena_query_boundary(input: &EngineInputV2) -> OmenaQueryBoundarySummaryV0 {
106 let fragment_bundle = summarize_omena_query_fragment_bundle(input);
107 let expression_semantics_query_count = fragment_bundle.expression_semantics.fragments.len();
108 let source_resolution_query_count = fragment_bundle.source_resolution.fragments.len();
109 let selector_usage_query_count = fragment_bundle.selector_usage.fragments.len();
110
111 OmenaQueryBoundarySummaryV0 {
112 schema_version: "0",
113 product: "omena-query.boundary",
114 query_engine_name: "omena-query",
115 input_version: input.version.clone(),
116 abstract_value_domain: summarize_omena_abstract_value_domain(),
117 selected_query_adapter_capabilities:
118 summarize_omena_query_selected_query_adapter_capabilities(),
119 delegated_fragment_products: vec![
120 "engine-input-producers.expression-semantics-query-fragments",
121 "engine-input-producers.source-resolution-query-fragments",
122 "omena-resolver.boundary",
123 "engine-input-producers.selector-usage-query-fragments",
124 "engine-input-producers.expression-domain-flow-analysis",
125 ],
126 expression_semantics_query_count,
127 source_resolution_query_count,
128 selector_usage_query_count,
129 total_query_count: expression_semantics_query_count
130 + source_resolution_query_count
131 + selector_usage_query_count,
132 ready_surfaces: vec![
133 "queryFragmentBundle",
134 "abstractValueProjectionContract",
135 "sourceResolutionResolverBoundary",
136 "expressionDomainFlowAnalysisBoundary",
137 "queryBoundarySummary",
138 ],
139 cme_coupled_surfaces: vec!["EngineInputV2", "producerQueryFragments"],
140 next_decoupling_targets: vec!["queryEvaluationRuntime", "selectedQueryBackendAdapter"],
141 }
142}
143
144pub fn summarize_omena_query_selected_query_adapter_capabilities()
145-> SelectedQueryAdapterCapabilitiesV0 {
146 SelectedQueryAdapterCapabilitiesV0 {
147 schema_version: "0",
148 product: "omena-query.selected-query-adapter-capabilities",
149 default_candidate_backend: "rust-selected-query",
150 backend_kinds: vec![
151 SelectedQueryBackendCapabilityV0 {
152 backend_kind: "typescript-current",
153 source_resolution: false,
154 expression_semantics: false,
155 selector_usage: false,
156 style_semantic_graph: false,
157 },
158 SelectedQueryBackendCapabilityV0 {
159 backend_kind: "rust-source-resolution",
160 source_resolution: true,
161 expression_semantics: false,
162 selector_usage: false,
163 style_semantic_graph: false,
164 },
165 SelectedQueryBackendCapabilityV0 {
166 backend_kind: "rust-expression-semantics",
167 source_resolution: false,
168 expression_semantics: true,
169 selector_usage: false,
170 style_semantic_graph: false,
171 },
172 SelectedQueryBackendCapabilityV0 {
173 backend_kind: "rust-selector-usage",
174 source_resolution: false,
175 expression_semantics: false,
176 selector_usage: true,
177 style_semantic_graph: false,
178 },
179 SelectedQueryBackendCapabilityV0 {
180 backend_kind: "rust-selected-query",
181 source_resolution: true,
182 expression_semantics: true,
183 selector_usage: true,
184 style_semantic_graph: true,
185 },
186 ],
187 runner_commands: vec![
188 SelectedQueryRunnerCommandV0 {
189 surface: "sourceResolution",
190 command: "input-source-resolution-canonical-producer",
191 input_contract: "EngineInputV2",
192 output_product: "engine-input-producers.source-resolution-canonical-producer",
193 },
194 SelectedQueryRunnerCommandV0 {
195 surface: "expressionSemantics",
196 command: "input-expression-semantics-canonical-producer",
197 input_contract: "EngineInputV2",
198 output_product: "engine-input-producers.expression-semantics-canonical-producer",
199 },
200 SelectedQueryRunnerCommandV0 {
201 surface: "expressionDomainFlowAnalysis",
202 command: "input-expression-domain-flow-analysis",
203 input_contract: "EngineInputV2",
204 output_product: "engine-input-producers.expression-domain-flow-analysis",
205 },
206 SelectedQueryRunnerCommandV0 {
207 surface: "selectorUsage",
208 command: "input-selector-usage-canonical-producer",
209 input_contract: "EngineInputV2",
210 output_product: "engine-input-producers.selector-usage-canonical-producer",
211 },
212 SelectedQueryRunnerCommandV0 {
213 surface: "styleSemanticGraph",
214 command: "style-semantic-graph",
215 input_contract: "StyleSemanticGraphInputV0",
216 output_product: "omena-semantic.style-semantic-graph",
217 },
218 SelectedQueryRunnerCommandV0 {
219 surface: "styleSemanticGraphBatch",
220 command: "style-semantic-graph-batch",
221 input_contract: "StyleSemanticGraphBatchInputV0",
222 output_product: "omena-semantic.style-semantic-graph-batch",
223 },
224 ],
225 expression_semantics_payload_contracts: vec!["valueDomainKind", "valueDomainDerivation"],
226 required_input_contracts: vec![
227 "EngineInputV2",
228 "StyleSemanticGraphInputV0",
229 "StyleSemanticGraphBatchInputV0",
230 ],
231 adapter_readiness: vec![
232 "backendCapabilityMatrix",
233 "canonicalProducerWrapperBoundary",
234 "styleSemanticGraphBridgeBoundary",
235 "runnerCommandContract",
236 "fragmentBundleBoundary",
237 "expressionSemanticsDerivationPayload",
238 "expressionDomainFlowAnalysisRunner",
239 ],
240 routing_status: "declaredOnly",
241 }
242}
243
244pub fn summarize_omena_query_fragment_bundle(input: &EngineInputV2) -> OmenaQueryFragmentBundleV0 {
245 OmenaQueryFragmentBundleV0 {
246 schema_version: "0",
247 product: "omena-query.fragment-bundle",
248 input_version: input.version.clone(),
249 expression_semantics: summarize_omena_query_expression_semantics_query_fragments(input),
250 source_resolution: summarize_omena_query_source_resolution_query_fragments(input),
251 selector_usage: summarize_omena_query_selector_usage_query_fragments(input),
252 }
253}
254
255pub fn summarize_omena_query_expression_semantics_query_fragments(
256 input: &EngineInputV2,
257) -> ExpressionSemanticsQueryFragmentsV0 {
258 summarize_expression_semantics_query_fragments_input(input)
259}
260
261pub fn summarize_omena_query_expression_domain_flow_analysis(
262 input: &EngineInputV2,
263) -> ExpressionDomainFlowAnalysisV0 {
264 summarize_expression_domain_flow_analysis_input(input)
265}
266
267pub fn summarize_omena_query_source_resolution_query_fragments(
268 input: &EngineInputV2,
269) -> SourceResolutionQueryFragmentsV0 {
270 summarize_omena_resolver_query_fragments(input)
271}
272
273pub fn summarize_omena_query_selector_usage_query_fragments(
274 input: &EngineInputV2,
275) -> SelectorUsageQueryFragmentsV0 {
276 summarize_selector_usage_query_fragments_input(input)
277}
278
279pub fn summarize_omena_query_source_resolution_canonical_producer_signal(
280 input: &EngineInputV2,
281) -> SourceResolutionCanonicalProducerSignalV0 {
282 summarize_omena_resolver_canonical_producer_signal(input)
283}
284
285pub fn summarize_omena_query_expression_semantics_canonical_producer_signal(
286 input: &EngineInputV2,
287) -> ExpressionSemanticsCanonicalProducerSignalV0 {
288 summarize_expression_semantics_canonical_producer_signal_input(input)
289}
290
291pub fn summarize_omena_query_selector_usage_canonical_producer_signal(
292 input: &EngineInputV2,
293) -> SelectorUsageCanonicalProducerSignalV0 {
294 summarize_selector_usage_canonical_producer_signal_input(input)
295}
296
297pub fn summarize_omena_query_style_semantic_graph_from_source(
298 style_path: &str,
299 style_source: &str,
300 input: &EngineInputV2,
301) -> Option<StyleSemanticGraphSummaryV0> {
302 summarize_omena_bridge_style_semantic_graph_from_source(style_path, style_source, input)
303}
304
305pub fn summarize_omena_query_style_semantic_graph_batch_from_sources<'a>(
306 styles: impl IntoIterator<Item = (&'a str, &'a str)>,
307 input: &EngineInputV2,
308) -> OmenaQueryStyleSemanticGraphBatchOutputV0 {
309 let style_sources = styles.into_iter().collect::<Vec<_>>();
310 let parsed_styles = style_sources
311 .iter()
312 .filter_map(|(style_path, style_source)| {
313 parse_style_module(style_path, style_source)
314 .map(|sheet| ((*style_path).to_string(), sheet))
315 })
316 .collect::<Vec<_>>();
317 let workspace_declarations = parsed_styles
318 .iter()
319 .flat_map(|(style_path, sheet)| {
320 collect_omena_bridge_design_token_workspace_declarations(style_path, sheet)
321 })
322 .collect::<Vec<_>>();
323 let graphs = style_sources
324 .into_iter()
325 .map(
326 |(style_path, _style_source)| OmenaQueryStyleSemanticGraphBatchEntryV0 {
327 style_path: style_path.to_string(),
328 graph: parsed_style_by_path(&parsed_styles, style_path).map(|sheet| {
329 let import_reachable_declarations =
330 filter_import_reachable_design_token_workspace_declarations(
331 style_path,
332 &parsed_styles,
333 &workspace_declarations,
334 );
335 summarize_omena_bridge_style_semantic_graph_for_path_with_scoped_workspace_declarations(
336 sheet,
337 input,
338 Some(style_path),
339 &import_reachable_declarations,
340 DesignTokenExternalDeclarationCandidateScopeV0::CrossFileImportGraph,
341 )
342 }),
343 },
344 )
345 .collect::<Vec<_>>();
346
347 OmenaQueryStyleSemanticGraphBatchOutputV0 {
348 schema_version: "0",
349 product: "omena-semantic.style-semantic-graph-batch",
350 graphs,
351 }
352}
353
354fn parsed_style_by_path<'a>(
355 parsed_styles: &'a [(String, Stylesheet)],
356 style_path: &str,
357) -> Option<&'a Stylesheet> {
358 parsed_styles
359 .iter()
360 .find(|(parsed_style_path, _sheet)| parsed_style_path == style_path)
361 .map(|(_style_path, sheet)| sheet)
362}
363
364fn filter_import_reachable_design_token_workspace_declarations(
365 target_style_path: &str,
366 parsed_styles: &[(String, Stylesheet)],
367 workspace_declarations: &[DesignTokenWorkspaceDeclarationFactV0],
368) -> Vec<DesignTokenWorkspaceDeclarationFactV0> {
369 let reachable_style_paths =
370 collect_import_reachable_style_path_metadata(target_style_path, parsed_styles);
371 workspace_declarations
372 .iter()
373 .filter_map(|declaration| {
374 if declaration.file_path == target_style_path {
375 return Some(declaration.clone());
376 }
377 let reachability = reachable_style_paths.get(declaration.file_path.as_str())?;
378 let mut declaration = declaration.clone();
379 declaration.import_graph_distance = Some(reachability.distance);
380 declaration.import_graph_order = Some(reachability.order);
381 Some(declaration)
382 })
383 .collect()
384}
385
386#[derive(Debug, Clone, Copy, PartialEq, Eq)]
387struct ImportReachability {
388 distance: usize,
389 order: usize,
390}
391
392fn collect_import_reachable_style_path_metadata(
393 target_style_path: &str,
394 parsed_styles: &[(String, Stylesheet)],
395) -> BTreeMap<String, ImportReachability> {
396 let mut reachable_style_paths = BTreeMap::new();
397 let available_style_paths = parsed_styles
398 .iter()
399 .map(|(style_path, _sheet)| style_path.as_str())
400 .collect::<BTreeSet<_>>();
401 let mut pending_style_paths = collect_import_reachable_direct_style_paths(
402 target_style_path,
403 parsed_styles,
404 &available_style_paths,
405 )
406 .into_iter()
407 .map(|style_path| (style_path, 1usize))
408 .collect::<VecDeque<_>>();
409 let style_by_path = parsed_styles
410 .iter()
411 .map(|(style_path, sheet)| (style_path.as_str(), sheet))
412 .collect::<BTreeMap<_, _>>();
413 let mut visit_order = 0usize;
414
415 while let Some((style_path, distance)) = pending_style_paths.pop_front() {
416 if style_path == target_style_path || reachable_style_paths.contains_key(&style_path) {
417 continue;
418 }
419 reachable_style_paths.insert(
420 style_path.clone(),
421 ImportReachability {
422 distance,
423 order: visit_order,
424 },
425 );
426 visit_order += 1;
427
428 let Some(sheet) = style_by_path.get(style_path.as_str()) else {
429 continue;
430 };
431 for source in collect_sass_module_sources(sheet) {
432 if let Some(next_style_path) =
433 resolve_style_module_source(&style_path, &source, &available_style_paths)
434 {
435 pending_style_paths.push_back((next_style_path, distance + 1));
436 }
437 }
438 }
439
440 reachable_style_paths
441}
442
443fn collect_import_reachable_direct_style_paths(
444 target_style_path: &str,
445 parsed_styles: &[(String, Stylesheet)],
446 available_style_paths: &BTreeSet<&str>,
447) -> Vec<String> {
448 let Some(target_sheet) = parsed_style_by_path(parsed_styles, target_style_path) else {
449 return Vec::new();
450 };
451 collect_sass_module_sources(target_sheet)
452 .into_iter()
453 .filter_map(|source| {
454 resolve_style_module_source(target_style_path, &source, available_style_paths)
455 })
456 .collect()
457}
458
459fn collect_sass_module_sources(sheet: &Stylesheet) -> Vec<String> {
460 let summary = summarize_css_modules_intermediate(sheet);
461 let mut sources = Vec::new();
462 for edge in summary.sass.module_use_edges {
463 push_unique_string(&mut sources, edge.source);
464 }
465 for source in summary.sass.module_forward_sources {
466 push_unique_string(&mut sources, source);
467 }
468 for source in summary.sass.module_import_sources {
469 push_unique_string(&mut sources, source);
470 }
471 sources
472}
473
474fn resolve_style_module_source(
475 from_style_path: &str,
476 source: &str,
477 available_style_paths: &BTreeSet<&str>,
478) -> Option<String> {
479 if source.starts_with("sass:")
480 || source.starts_with("http://")
481 || source.starts_with("https://")
482 {
483 return None;
484 }
485
486 style_module_source_candidates(from_style_path, source)
487 .into_iter()
488 .find(|candidate| available_style_paths.contains(candidate.as_str()))
489}
490
491fn style_module_source_candidates(from_style_path: &str, source: &str) -> Vec<String> {
492 let source_path = Path::new(source);
493 let base_path = if source_path.is_absolute() {
494 PathBuf::from(source)
495 } else {
496 Path::new(from_style_path)
497 .parent()
498 .map(|parent| parent.join(source))
499 .unwrap_or_else(|| PathBuf::from(source))
500 };
501 let mut candidates = Vec::new();
502 push_style_path_candidate(&mut candidates, base_path.clone());
503 push_partial_style_path_candidate(&mut candidates, &base_path);
504
505 if source_path.extension().is_none() {
506 for extension in [
507 ".module.scss",
508 ".module.css",
509 ".module.less",
510 ".scss",
511 ".css",
512 ".less",
513 ] {
514 let candidate = PathBuf::from(format!("{}{}", base_path.display(), extension));
515 push_style_path_candidate(&mut candidates, candidate.clone());
516 push_partial_style_path_candidate(&mut candidates, &candidate);
517 }
518 }
519
520 candidates
521}
522
523fn push_partial_style_path_candidate(candidates: &mut Vec<String>, path: &Path) {
524 let Some(file_name) = path.file_name().and_then(|file_name| file_name.to_str()) else {
525 return;
526 };
527 if file_name.starts_with('_') {
528 return;
529 }
530 let mut partial_path = path.to_path_buf();
531 partial_path.set_file_name(format!("_{file_name}"));
532 push_style_path_candidate(candidates, partial_path);
533}
534
535fn push_style_path_candidate(candidates: &mut Vec<String>, path: PathBuf) {
536 let candidate = normalize_style_path(path);
537 if !candidates.contains(&candidate) {
538 candidates.push(candidate);
539 }
540}
541
542fn normalize_style_path(path: PathBuf) -> String {
543 let mut normalized = PathBuf::new();
544 for component in path.components() {
545 match component {
546 Component::CurDir => {}
547 Component::ParentDir => {
548 normalized.pop();
549 }
550 Component::Normal(part) => normalized.push(part),
551 Component::RootDir | Component::Prefix(_) => normalized.push(component.as_os_str()),
552 }
553 }
554 normalized.to_string_lossy().replace('\\', "/")
555}
556
557fn push_unique_string(values: &mut Vec<String>, value: String) {
558 if !values.contains(&value) {
559 values.push(value);
560 }
561}
562
563#[cfg(test)]
564mod tests {
565 use engine_input_producers::{
566 ClassExpressionInputV2, EngineInputV2, PositionV2, RangeV2, SourceAnalysisInputV2,
567 SourceDocumentV2, StringTypeFactsV2, StyleAnalysisInputV2, StyleDocumentV2,
568 StyleSelectorV2, TypeFactEntryV2,
569 };
570
571 use super::{
572 SelectedQueryAdapterCapabilitiesV0, summarize_omena_query_boundary,
573 summarize_omena_query_expression_domain_flow_analysis,
574 summarize_omena_query_expression_semantics_canonical_producer_signal,
575 summarize_omena_query_expression_semantics_query_fragments,
576 summarize_omena_query_fragment_bundle,
577 summarize_omena_query_selected_query_adapter_capabilities,
578 summarize_omena_query_selector_usage_canonical_producer_signal,
579 summarize_omena_query_selector_usage_query_fragments,
580 summarize_omena_query_source_resolution_canonical_producer_signal,
581 summarize_omena_query_source_resolution_query_fragments,
582 summarize_omena_query_style_semantic_graph_batch_from_sources,
583 summarize_omena_query_style_semantic_graph_from_source,
584 };
585
586 #[test]
587 fn summarizes_query_boundary_over_producer_fragments() {
588 let input = sample_input();
589 let summary = summarize_omena_query_boundary(&input);
590
591 assert_eq!(summary.schema_version, "0");
592 assert_eq!(summary.product, "omena-query.boundary");
593 assert_eq!(summary.query_engine_name, "omena-query");
594 assert_eq!(summary.input_version, "2");
595 assert_eq!(
596 summary.abstract_value_domain.product,
597 "omena-abstract-value.domain"
598 );
599 assert_eq!(
600 summary.selected_query_adapter_capabilities.product,
601 "omena-query.selected-query-adapter-capabilities"
602 );
603 assert_eq!(summary.expression_semantics_query_count, 2);
604 assert_eq!(summary.source_resolution_query_count, 2);
605 assert_eq!(summary.selector_usage_query_count, 2);
606 assert_eq!(summary.total_query_count, 6);
607 assert!(
608 summary
609 .ready_surfaces
610 .contains(&"abstractValueProjectionContract")
611 );
612 assert!(
613 summary
614 .ready_surfaces
615 .contains(&"sourceResolutionResolverBoundary")
616 );
617 assert!(
618 summary
619 .delegated_fragment_products
620 .contains(&"omena-resolver.boundary")
621 );
622 assert!(
623 summary
624 .delegated_fragment_products
625 .contains(&"engine-input-producers.expression-domain-flow-analysis")
626 );
627 assert!(
628 summary
629 .ready_surfaces
630 .contains(&"expressionDomainFlowAnalysisBoundary")
631 );
632 assert!(
633 summary
634 .cme_coupled_surfaces
635 .contains(&"producerQueryFragments")
636 );
637 }
638
639 #[test]
640 fn bundles_expression_source_and_selector_query_fragments() {
641 let input = sample_input();
642 let bundle = summarize_omena_query_fragment_bundle(&input);
643
644 assert_eq!(bundle.schema_version, "0");
645 assert_eq!(bundle.product, "omena-query.fragment-bundle");
646 assert_eq!(bundle.input_version, "2");
647 assert_eq!(bundle.expression_semantics.fragments.len(), 2);
648 assert_eq!(bundle.expression_semantics.fragments[0].query_id, "expr-1");
649 assert_eq!(bundle.source_resolution.fragments.len(), 2);
650 assert_eq!(bundle.source_resolution.fragments[1].query_id, "expr-2");
651 assert_eq!(bundle.selector_usage.fragments.len(), 2);
652 assert_eq!(bundle.selector_usage.fragments[0].query_id, "btn-active");
653
654 let expression = summarize_omena_query_expression_semantics_query_fragments(&input);
655 let source = summarize_omena_query_source_resolution_query_fragments(&input);
656 let selector = summarize_omena_query_selector_usage_query_fragments(&input);
657
658 assert_eq!(expression.schema_version, "0");
659 assert_eq!(source.schema_version, "0");
660 assert_eq!(selector.schema_version, "0");
661 assert_eq!(expression.input_version, "2");
662 assert_eq!(source.input_version, "2");
663 assert_eq!(selector.input_version, "2");
664 assert_eq!(
665 expression.fragments.len(),
666 bundle.expression_semantics.fragments.len()
667 );
668 assert_eq!(
669 source.fragments.len(),
670 bundle.source_resolution.fragments.len()
671 );
672 assert_eq!(
673 selector.fragments.len(),
674 bundle.selector_usage.fragments.len()
675 );
676 }
677
678 #[test]
679 fn declares_selected_query_adapter_capabilities_without_flipping_runtime_routing() {
680 let summary = summarize_omena_query_selected_query_adapter_capabilities();
681
682 assert_eq!(summary.schema_version, "0");
683 assert_eq!(
684 summary.product,
685 "omena-query.selected-query-adapter-capabilities"
686 );
687 assert_eq!(summary.default_candidate_backend, "rust-selected-query");
688 assert_eq!(summary.routing_status, "declaredOnly");
689
690 let unified = backend(&summary, "rust-selected-query");
691 assert!(unified.is_some());
692 let Some(unified) = unified else {
693 return;
694 };
695 assert!(unified.source_resolution);
696 assert!(unified.expression_semantics);
697 assert!(unified.selector_usage);
698 assert!(unified.style_semantic_graph);
699
700 let source_only = backend(&summary, "rust-source-resolution");
701 assert!(source_only.is_some());
702 let Some(source_only) = source_only else {
703 return;
704 };
705 assert!(source_only.source_resolution);
706 assert!(!source_only.expression_semantics);
707 assert!(!source_only.selector_usage);
708 assert!(!source_only.style_semantic_graph);
709
710 assert!(
711 summary
712 .runner_commands
713 .iter()
714 .any(|command| command.command == "input-expression-domain-flow-analysis")
715 );
716 assert!(
717 summary
718 .runner_commands
719 .iter()
720 .any(|command| command.command == "style-semantic-graph-batch")
721 );
722 assert!(
723 summary
724 .expression_semantics_payload_contracts
725 .contains(&"valueDomainDerivation")
726 );
727 assert!(summary.adapter_readiness.contains(&"runnerCommandContract"));
728 assert!(
729 summary
730 .adapter_readiness
731 .contains(&"canonicalProducerWrapperBoundary")
732 );
733 assert!(
734 summary
735 .adapter_readiness
736 .contains(&"styleSemanticGraphBridgeBoundary")
737 );
738 assert!(
739 summary
740 .adapter_readiness
741 .contains(&"expressionDomainFlowAnalysisRunner")
742 );
743 }
744
745 #[test]
746 fn owns_expression_domain_flow_analysis_wrapper_without_changing_product() {
747 let input = sample_input();
748 let summary = summarize_omena_query_expression_domain_flow_analysis(&input);
749
750 assert_eq!(summary.schema_version, "0");
751 assert_eq!(
752 summary.product,
753 "engine-input-producers.expression-domain-flow-analysis"
754 );
755 assert_eq!(summary.input_version, "2");
756 assert_eq!(summary.analyses.len(), 2);
757 assert!(
758 summary
759 .analyses
760 .iter()
761 .all(|entry| entry.analysis.product == "omena-abstract-value.flow-analysis")
762 );
763 assert!(
764 summary
765 .analyses
766 .iter()
767 .all(|entry| entry.analysis.converged)
768 );
769 }
770
771 #[test]
772 fn owns_selected_query_canonical_producer_wrappers_without_changing_products() {
773 let input = sample_input();
774
775 let source = summarize_omena_query_source_resolution_canonical_producer_signal(&input);
776 assert_eq!(source.schema_version, "0");
777 assert_eq!(source.input_version, "2");
778 assert_eq!(source.canonical_bundle.query_fragments.len(), 2);
779 assert_eq!(source.evaluator_candidates.results.len(), 2);
780
781 let expression =
782 summarize_omena_query_expression_semantics_canonical_producer_signal(&input);
783 assert_eq!(expression.schema_version, "0");
784 assert_eq!(expression.input_version, "2");
785 assert_eq!(expression.canonical_bundle.query_fragments.len(), 2);
786 assert_eq!(expression.evaluator_candidates.results.len(), 2);
787 assert_eq!(
788 expression.evaluator_candidates.results[0]
789 .payload
790 .value_domain_derivation
791 .product,
792 "omena-abstract-value.reduced-class-value-derivation"
793 );
794 assert_eq!(
795 expression.evaluator_candidates.results[0]
796 .payload
797 .value_domain_derivation
798 .reduced_kind,
799 "prefixSuffix"
800 );
801
802 let selector = summarize_omena_query_selector_usage_canonical_producer_signal(&input);
803 assert_eq!(selector.schema_version, "0");
804 assert_eq!(selector.input_version, "2");
805 assert_eq!(selector.canonical_bundle.query_fragments.len(), 2);
806 assert_eq!(selector.evaluator_candidates.results.len(), 2);
807 }
808
809 #[test]
810 fn owns_style_semantic_graph_adapter_boundary_without_changing_graph_product() {
811 let input = sample_input();
812 let graph = summarize_omena_query_style_semantic_graph_from_source(
813 "/tmp/App.module.scss",
814 ".btn-active { color: red; }",
815 &input,
816 );
817 assert!(graph.is_some());
818 let Some(graph) = graph else {
819 return;
820 };
821 assert_eq!(graph.schema_version, "0");
822 assert_eq!(graph.product, "omena-semantic.style-semantic-graph");
823 assert_eq!(graph.selector_identity_engine.canonical_ids.len(), 1);
824
825 let batch = summarize_omena_query_style_semantic_graph_batch_from_sources(
826 [
827 ("/tmp/App.module.scss", ".btn-active { color: red; }"),
828 ("/tmp/Card.module.scss", ".card-header { color: blue; }"),
829 ],
830 &input,
831 );
832 assert_eq!(batch.schema_version, "0");
833 assert_eq!(batch.product, "omena-semantic.style-semantic-graph-batch");
834 assert_eq!(batch.graphs.len(), 2);
835 assert_eq!(batch.graphs[0].style_path, "/tmp/App.module.scss");
836 assert!(batch.graphs[0].graph.is_some());
837 assert!(batch.graphs[1].graph.is_some());
838 }
839
840 #[test]
841 fn style_semantic_graph_batch_feeds_workspace_design_token_candidates() {
842 let input = sample_input();
843 let batch = summarize_omena_query_style_semantic_graph_batch_from_sources(
844 [
845 ("/tmp/tokens.module.scss", ":root { --brand: red; }"),
846 ("/tmp/theme.module.scss", "@forward \"./tokens\";"),
847 ("/tmp/unrelated.module.scss", ":root { --brand: blue; }"),
848 (
849 "/tmp/App.module.scss",
850 "@use \"./theme\";\n.button { color: var(--brand); }",
851 ),
852 ],
853 &input,
854 );
855
856 let app_graph = batch
857 .graphs
858 .iter()
859 .find(|entry| entry.style_path == "/tmp/App.module.scss")
860 .and_then(|entry| entry.graph.as_ref());
861 assert!(app_graph.is_some());
862 let Some(app_graph) = app_graph else {
863 return;
864 };
865 let design_tokens = &app_graph.design_token_semantics;
866
867 assert_eq!(
868 design_tokens.status,
869 "cross-file-import-cascade-ranking-seed"
870 );
871 assert_eq!(
872 design_tokens.resolution_scope,
873 "cross-file-import-candidate"
874 );
875 assert!(
876 design_tokens
877 .capabilities
878 .workspace_cascade_candidate_signal_ready
879 );
880 assert!(design_tokens.capabilities.cross_file_import_graph_ready);
881 assert_eq!(
882 design_tokens
883 .resolution_signal
884 .cross_file_declaration_fact_count,
885 1
886 );
887 assert_eq!(
888 design_tokens
889 .resolution_signal
890 .workspace_occurrence_resolved_reference_count,
891 1
892 );
893 assert_eq!(
894 design_tokens
895 .cascade_ranking_signal
896 .cross_file_candidate_declaration_count,
897 1
898 );
899 assert_eq!(
900 design_tokens
901 .cascade_ranking_signal
902 .cross_file_winner_declaration_count,
903 1
904 );
905 assert_eq!(
906 design_tokens.cascade_ranking_signal.ranked_references[0]
907 .winner_declaration_file_path
908 .as_deref(),
909 Some("/tmp/tokens.module.scss")
910 );
911 let winner_range =
912 design_tokens.cascade_ranking_signal.ranked_references[0].winner_declaration_range;
913 assert_eq!(winner_range.map(|range| range.start.line), Some(0));
914 assert_eq!(winner_range.map(|range| range.start.character), Some(8));
915 assert_eq!(
916 design_tokens.cascade_ranking_signal.ranked_references[0]
917 .cross_file_candidate_declaration_count,
918 1
919 );
920 }
921
922 #[test]
923 fn style_semantic_graph_batch_prefers_nearer_import_graph_token_candidates() {
924 let input = sample_input();
925 let batch = summarize_omena_query_style_semantic_graph_batch_from_sources(
926 [
927 ("/tmp/a-direct.module.scss", ":root { --brand: direct; }"),
928 ("/tmp/mid.module.scss", "@forward \"./z-transitive\";"),
929 (
930 "/tmp/z-transitive.module.scss",
931 ":root { --brand: transitive; }",
932 ),
933 (
934 "/tmp/App.module.scss",
935 "@use \"./a-direct\";\n@use \"./mid\";\n.button { color: var(--brand); }",
936 ),
937 ],
938 &input,
939 );
940
941 let app_graph = batch
942 .graphs
943 .iter()
944 .find(|entry| entry.style_path == "/tmp/App.module.scss")
945 .and_then(|entry| entry.graph.as_ref());
946 assert!(app_graph.is_some());
947 let Some(app_graph) = app_graph else {
948 return;
949 };
950 let ranked_reference = &app_graph
951 .design_token_semantics
952 .cascade_ranking_signal
953 .ranked_references[0];
954
955 assert_eq!(
956 ranked_reference.winner_declaration_file_path.as_deref(),
957 Some("/tmp/a-direct.module.scss")
958 );
959 assert_eq!(ranked_reference.winner_import_graph_distance, Some(1));
960 assert_eq!(ranked_reference.winner_import_graph_order, Some(0));
961 assert_eq!(ranked_reference.cross_file_candidate_declaration_count, 2);
962 assert_eq!(ranked_reference.cross_file_shadowed_declaration_count, 1);
963 }
964
965 fn backend<'a>(
966 summary: &'a SelectedQueryAdapterCapabilitiesV0,
967 backend_kind: &str,
968 ) -> Option<&'a super::SelectedQueryBackendCapabilityV0> {
969 summary
970 .backend_kinds
971 .iter()
972 .find(|backend| backend.backend_kind == backend_kind)
973 }
974
975 fn sample_input() -> EngineInputV2 {
976 EngineInputV2 {
977 version: "2".to_string(),
978 sources: vec![SourceAnalysisInputV2 {
979 document: SourceDocumentV2 {
980 class_expressions: vec![
981 ClassExpressionInputV2 {
982 id: "expr-1".to_string(),
983 kind: "symbolRef".to_string(),
984 scss_module_path: "/tmp/App.module.scss".to_string(),
985 range: range(4, 12, 4, 16),
986 class_name: None,
987 root_binding_decl_id: Some("decl-1".to_string()),
988 access_path: None,
989 },
990 ClassExpressionInputV2 {
991 id: "expr-2".to_string(),
992 kind: "styleAccess".to_string(),
993 scss_module_path: "/tmp/Card.module.scss".to_string(),
994 range: range(6, 9, 6, 20),
995 class_name: Some("card-header".to_string()),
996 root_binding_decl_id: None,
997 access_path: Some(vec!["card".to_string(), "header".to_string()]),
998 },
999 ],
1000 },
1001 }],
1002 styles: vec![
1003 StyleAnalysisInputV2 {
1004 file_path: "/tmp/App.module.scss".to_string(),
1005 document: StyleDocumentV2 {
1006 selectors: vec![StyleSelectorV2 {
1007 name: "btn-active".to_string(),
1008 view_kind: "canonical".to_string(),
1009 canonical_name: Some("btn-active".to_string()),
1010 range: range(1, 1, 1, 12),
1011 nested_safety: Some("safe".to_string()),
1012 composes: None,
1013 bem_suffix: None,
1014 }],
1015 },
1016 },
1017 StyleAnalysisInputV2 {
1018 file_path: "/tmp/Card.module.scss".to_string(),
1019 document: StyleDocumentV2 {
1020 selectors: vec![StyleSelectorV2 {
1021 name: "card-header".to_string(),
1022 view_kind: "canonical".to_string(),
1023 canonical_name: Some("card-header".to_string()),
1024 range: range(3, 1, 3, 13),
1025 nested_safety: Some("unsafe".to_string()),
1026 composes: None,
1027 bem_suffix: None,
1028 }],
1029 },
1030 },
1031 ],
1032 type_facts: vec![
1033 TypeFactEntryV2 {
1034 file_path: "/tmp/App.tsx".to_string(),
1035 expression_id: "expr-1".to_string(),
1036 facts: StringTypeFactsV2 {
1037 kind: "constrained".to_string(),
1038 constraint_kind: Some("prefixSuffix".to_string()),
1039 values: None,
1040 prefix: Some("btn-".to_string()),
1041 suffix: Some("-active".to_string()),
1042 min_len: Some(10),
1043 max_len: None,
1044 char_must: None,
1045 char_may: None,
1046 may_include_other_chars: None,
1047 },
1048 },
1049 TypeFactEntryV2 {
1050 file_path: "/tmp/Card.tsx".to_string(),
1051 expression_id: "expr-2".to_string(),
1052 facts: StringTypeFactsV2 {
1053 kind: "finiteSet".to_string(),
1054 constraint_kind: None,
1055 values: Some(vec!["card-header".to_string(), "card-body".to_string()]),
1056 prefix: None,
1057 suffix: None,
1058 min_len: None,
1059 max_len: None,
1060 char_must: None,
1061 char_may: None,
1062 may_include_other_chars: None,
1063 },
1064 },
1065 ],
1066 }
1067 }
1068
1069 fn range(
1070 start_line: usize,
1071 start_character: usize,
1072 end_line: usize,
1073 end_character: usize,
1074 ) -> RangeV2 {
1075 RangeV2 {
1076 start: PositionV2 {
1077 line: start_line,
1078 character: start_character,
1079 },
1080 end: PositionV2 {
1081 line: end_line,
1082 character: end_character,
1083 },
1084 }
1085 }
1086}