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_module_path_candidates(
503 &mut candidates,
504 base_path,
505 source_path.extension().is_none(),
506 );
507 for package_base_path in package_style_module_base_candidates(from_style_path, source) {
508 push_style_module_path_candidates(&mut candidates, package_base_path, true);
509 }
510
511 candidates
512}
513
514fn push_style_module_path_candidates(
515 candidates: &mut Vec<String>,
516 base_path: PathBuf,
517 include_extension_variants: bool,
518) {
519 push_style_path_candidate(candidates, base_path.clone());
520 push_partial_style_path_candidate(candidates, &base_path);
521
522 if !include_extension_variants {
523 return;
524 }
525
526 for extension in [
527 ".module.scss",
528 ".module.css",
529 ".module.less",
530 ".scss",
531 ".css",
532 ".less",
533 ] {
534 let candidate = PathBuf::from(format!("{}{}", base_path.display(), extension));
535 push_style_path_candidate(candidates, candidate.clone());
536 push_partial_style_path_candidate(candidates, &candidate);
537 }
538}
539
540fn package_style_module_base_candidates(from_style_path: &str, source: &str) -> Vec<PathBuf> {
541 let Some(package_source) = parse_package_style_source(source) else {
542 return Vec::new();
543 };
544 let Some(from_dir) = Path::new(from_style_path).parent() else {
545 return Vec::new();
546 };
547 let mut candidates = Vec::new();
548 let mut current_dir = Some(from_dir);
549 while let Some(dir) = current_dir {
550 let package_root = dir.join("node_modules").join(package_source.package_name);
551 let package_entry = match package_source.subpath {
552 Some(subpath) => package_root.join(subpath),
553 None => package_root.clone(),
554 };
555 push_unique_pathbuf(&mut candidates, package_entry.clone());
556 if let Some(subpath) = package_source.subpath {
557 push_unique_pathbuf(&mut candidates, package_root.join("src").join(subpath));
558 } else {
559 push_unique_pathbuf(&mut candidates, package_root.join("index"));
560 push_unique_pathbuf(&mut candidates, package_root.join("src").join("index"));
561 }
562 current_dir = dir.parent();
563 }
564 candidates
565}
566
567#[derive(Debug, Clone, Copy, PartialEq, Eq)]
568struct PackageStyleSource<'a> {
569 package_name: &'a str,
570 subpath: Option<&'a str>,
571}
572
573fn parse_package_style_source(source: &str) -> Option<PackageStyleSource<'_>> {
574 if source.starts_with('.')
575 || source.starts_with('/')
576 || source.starts_with("sass:")
577 || source.starts_with("http://")
578 || source.starts_with("https://")
579 {
580 return None;
581 }
582
583 if source.starts_with('@') {
584 let mut segments = source.splitn(3, '/');
585 let scope = segments.next()?;
586 let package = segments.next()?;
587 if scope.len() <= 1 || package.is_empty() {
588 return None;
589 }
590 let package_name_end = scope.len() + 1 + package.len();
591 let package_name = &source[..package_name_end];
592 let subpath = segments.next().filter(|subpath| !subpath.is_empty());
593 return Some(PackageStyleSource {
594 package_name,
595 subpath,
596 });
597 }
598
599 let mut segments = source.splitn(2, '/');
600 let package_name = segments.next()?;
601 if package_name.is_empty() {
602 return None;
603 }
604 let subpath = segments.next().filter(|subpath| !subpath.is_empty());
605 Some(PackageStyleSource {
606 package_name,
607 subpath,
608 })
609}
610
611fn push_unique_pathbuf(candidates: &mut Vec<PathBuf>, value: PathBuf) {
612 if !candidates.contains(&value) {
613 candidates.push(value);
614 }
615}
616
617fn push_partial_style_path_candidate(candidates: &mut Vec<String>, path: &Path) {
618 let Some(file_name) = path.file_name().and_then(|file_name| file_name.to_str()) else {
619 return;
620 };
621 if file_name.starts_with('_') {
622 return;
623 }
624 let mut partial_path = path.to_path_buf();
625 partial_path.set_file_name(format!("_{file_name}"));
626 push_style_path_candidate(candidates, partial_path);
627}
628
629fn push_style_path_candidate(candidates: &mut Vec<String>, path: PathBuf) {
630 let candidate = normalize_style_path(path);
631 if !candidates.contains(&candidate) {
632 candidates.push(candidate);
633 }
634}
635
636fn normalize_style_path(path: PathBuf) -> String {
637 let mut normalized = PathBuf::new();
638 for component in path.components() {
639 match component {
640 Component::CurDir => {}
641 Component::ParentDir => {
642 normalized.pop();
643 }
644 Component::Normal(part) => normalized.push(part),
645 Component::RootDir | Component::Prefix(_) => normalized.push(component.as_os_str()),
646 }
647 }
648 normalized.to_string_lossy().replace('\\', "/")
649}
650
651fn push_unique_string(values: &mut Vec<String>, value: String) {
652 if !values.contains(&value) {
653 values.push(value);
654 }
655}
656
657#[cfg(test)]
658mod tests {
659 use engine_input_producers::{
660 ClassExpressionInputV2, EngineInputV2, PositionV2, RangeV2, SourceAnalysisInputV2,
661 SourceDocumentV2, StringTypeFactsV2, StyleAnalysisInputV2, StyleDocumentV2,
662 StyleSelectorV2, TypeFactEntryV2,
663 };
664
665 use super::{
666 SelectedQueryAdapterCapabilitiesV0, summarize_omena_query_boundary,
667 summarize_omena_query_expression_domain_flow_analysis,
668 summarize_omena_query_expression_semantics_canonical_producer_signal,
669 summarize_omena_query_expression_semantics_query_fragments,
670 summarize_omena_query_fragment_bundle,
671 summarize_omena_query_selected_query_adapter_capabilities,
672 summarize_omena_query_selector_usage_canonical_producer_signal,
673 summarize_omena_query_selector_usage_query_fragments,
674 summarize_omena_query_source_resolution_canonical_producer_signal,
675 summarize_omena_query_source_resolution_query_fragments,
676 summarize_omena_query_style_semantic_graph_batch_from_sources,
677 summarize_omena_query_style_semantic_graph_from_source,
678 };
679
680 #[test]
681 fn summarizes_query_boundary_over_producer_fragments() {
682 let input = sample_input();
683 let summary = summarize_omena_query_boundary(&input);
684
685 assert_eq!(summary.schema_version, "0");
686 assert_eq!(summary.product, "omena-query.boundary");
687 assert_eq!(summary.query_engine_name, "omena-query");
688 assert_eq!(summary.input_version, "2");
689 assert_eq!(
690 summary.abstract_value_domain.product,
691 "omena-abstract-value.domain"
692 );
693 assert_eq!(
694 summary.selected_query_adapter_capabilities.product,
695 "omena-query.selected-query-adapter-capabilities"
696 );
697 assert_eq!(summary.expression_semantics_query_count, 2);
698 assert_eq!(summary.source_resolution_query_count, 2);
699 assert_eq!(summary.selector_usage_query_count, 2);
700 assert_eq!(summary.total_query_count, 6);
701 assert!(
702 summary
703 .ready_surfaces
704 .contains(&"abstractValueProjectionContract")
705 );
706 assert!(
707 summary
708 .ready_surfaces
709 .contains(&"sourceResolutionResolverBoundary")
710 );
711 assert!(
712 summary
713 .delegated_fragment_products
714 .contains(&"omena-resolver.boundary")
715 );
716 assert!(
717 summary
718 .delegated_fragment_products
719 .contains(&"engine-input-producers.expression-domain-flow-analysis")
720 );
721 assert!(
722 summary
723 .ready_surfaces
724 .contains(&"expressionDomainFlowAnalysisBoundary")
725 );
726 assert!(
727 summary
728 .cme_coupled_surfaces
729 .contains(&"producerQueryFragments")
730 );
731 }
732
733 #[test]
734 fn bundles_expression_source_and_selector_query_fragments() {
735 let input = sample_input();
736 let bundle = summarize_omena_query_fragment_bundle(&input);
737
738 assert_eq!(bundle.schema_version, "0");
739 assert_eq!(bundle.product, "omena-query.fragment-bundle");
740 assert_eq!(bundle.input_version, "2");
741 assert_eq!(bundle.expression_semantics.fragments.len(), 2);
742 assert_eq!(bundle.expression_semantics.fragments[0].query_id, "expr-1");
743 assert_eq!(bundle.source_resolution.fragments.len(), 2);
744 assert_eq!(bundle.source_resolution.fragments[1].query_id, "expr-2");
745 assert_eq!(bundle.selector_usage.fragments.len(), 2);
746 assert_eq!(bundle.selector_usage.fragments[0].query_id, "btn-active");
747
748 let expression = summarize_omena_query_expression_semantics_query_fragments(&input);
749 let source = summarize_omena_query_source_resolution_query_fragments(&input);
750 let selector = summarize_omena_query_selector_usage_query_fragments(&input);
751
752 assert_eq!(expression.schema_version, "0");
753 assert_eq!(source.schema_version, "0");
754 assert_eq!(selector.schema_version, "0");
755 assert_eq!(expression.input_version, "2");
756 assert_eq!(source.input_version, "2");
757 assert_eq!(selector.input_version, "2");
758 assert_eq!(
759 expression.fragments.len(),
760 bundle.expression_semantics.fragments.len()
761 );
762 assert_eq!(
763 source.fragments.len(),
764 bundle.source_resolution.fragments.len()
765 );
766 assert_eq!(
767 selector.fragments.len(),
768 bundle.selector_usage.fragments.len()
769 );
770 }
771
772 #[test]
773 fn declares_selected_query_adapter_capabilities_without_flipping_runtime_routing() {
774 let summary = summarize_omena_query_selected_query_adapter_capabilities();
775
776 assert_eq!(summary.schema_version, "0");
777 assert_eq!(
778 summary.product,
779 "omena-query.selected-query-adapter-capabilities"
780 );
781 assert_eq!(summary.default_candidate_backend, "rust-selected-query");
782 assert_eq!(summary.routing_status, "declaredOnly");
783
784 let unified = backend(&summary, "rust-selected-query");
785 assert!(unified.is_some());
786 let Some(unified) = unified else {
787 return;
788 };
789 assert!(unified.source_resolution);
790 assert!(unified.expression_semantics);
791 assert!(unified.selector_usage);
792 assert!(unified.style_semantic_graph);
793
794 let source_only = backend(&summary, "rust-source-resolution");
795 assert!(source_only.is_some());
796 let Some(source_only) = source_only else {
797 return;
798 };
799 assert!(source_only.source_resolution);
800 assert!(!source_only.expression_semantics);
801 assert!(!source_only.selector_usage);
802 assert!(!source_only.style_semantic_graph);
803
804 assert!(
805 summary
806 .runner_commands
807 .iter()
808 .any(|command| command.command == "input-expression-domain-flow-analysis")
809 );
810 assert!(
811 summary
812 .runner_commands
813 .iter()
814 .any(|command| command.command == "style-semantic-graph-batch")
815 );
816 assert!(
817 summary
818 .expression_semantics_payload_contracts
819 .contains(&"valueDomainDerivation")
820 );
821 assert!(summary.adapter_readiness.contains(&"runnerCommandContract"));
822 assert!(
823 summary
824 .adapter_readiness
825 .contains(&"canonicalProducerWrapperBoundary")
826 );
827 assert!(
828 summary
829 .adapter_readiness
830 .contains(&"styleSemanticGraphBridgeBoundary")
831 );
832 assert!(
833 summary
834 .adapter_readiness
835 .contains(&"expressionDomainFlowAnalysisRunner")
836 );
837 }
838
839 #[test]
840 fn owns_expression_domain_flow_analysis_wrapper_without_changing_product() {
841 let input = sample_input();
842 let summary = summarize_omena_query_expression_domain_flow_analysis(&input);
843
844 assert_eq!(summary.schema_version, "0");
845 assert_eq!(
846 summary.product,
847 "engine-input-producers.expression-domain-flow-analysis"
848 );
849 assert_eq!(summary.input_version, "2");
850 assert_eq!(summary.analyses.len(), 2);
851 assert!(
852 summary
853 .analyses
854 .iter()
855 .all(|entry| entry.analysis.product == "omena-abstract-value.flow-analysis")
856 );
857 assert!(
858 summary
859 .analyses
860 .iter()
861 .all(|entry| entry.analysis.converged)
862 );
863 }
864
865 #[test]
866 fn owns_selected_query_canonical_producer_wrappers_without_changing_products() {
867 let input = sample_input();
868
869 let source = summarize_omena_query_source_resolution_canonical_producer_signal(&input);
870 assert_eq!(source.schema_version, "0");
871 assert_eq!(source.input_version, "2");
872 assert_eq!(source.canonical_bundle.query_fragments.len(), 2);
873 assert_eq!(source.evaluator_candidates.results.len(), 2);
874
875 let expression =
876 summarize_omena_query_expression_semantics_canonical_producer_signal(&input);
877 assert_eq!(expression.schema_version, "0");
878 assert_eq!(expression.input_version, "2");
879 assert_eq!(expression.canonical_bundle.query_fragments.len(), 2);
880 assert_eq!(expression.evaluator_candidates.results.len(), 2);
881 assert_eq!(
882 expression.evaluator_candidates.results[0]
883 .payload
884 .value_domain_derivation
885 .product,
886 "omena-abstract-value.reduced-class-value-derivation"
887 );
888 assert_eq!(
889 expression.evaluator_candidates.results[0]
890 .payload
891 .value_domain_derivation
892 .reduced_kind,
893 "prefixSuffix"
894 );
895
896 let selector = summarize_omena_query_selector_usage_canonical_producer_signal(&input);
897 assert_eq!(selector.schema_version, "0");
898 assert_eq!(selector.input_version, "2");
899 assert_eq!(selector.canonical_bundle.query_fragments.len(), 2);
900 assert_eq!(selector.evaluator_candidates.results.len(), 2);
901 }
902
903 #[test]
904 fn owns_style_semantic_graph_adapter_boundary_without_changing_graph_product() {
905 let input = sample_input();
906 let graph = summarize_omena_query_style_semantic_graph_from_source(
907 "/tmp/App.module.scss",
908 ".btn-active { color: red; }",
909 &input,
910 );
911 assert!(graph.is_some());
912 let Some(graph) = graph else {
913 return;
914 };
915 assert_eq!(graph.schema_version, "0");
916 assert_eq!(graph.product, "omena-semantic.style-semantic-graph");
917 assert_eq!(graph.selector_identity_engine.canonical_ids.len(), 1);
918
919 let batch = summarize_omena_query_style_semantic_graph_batch_from_sources(
920 [
921 ("/tmp/App.module.scss", ".btn-active { color: red; }"),
922 ("/tmp/Card.module.scss", ".card-header { color: blue; }"),
923 ],
924 &input,
925 );
926 assert_eq!(batch.schema_version, "0");
927 assert_eq!(batch.product, "omena-semantic.style-semantic-graph-batch");
928 assert_eq!(batch.graphs.len(), 2);
929 assert_eq!(batch.graphs[0].style_path, "/tmp/App.module.scss");
930 assert!(batch.graphs[0].graph.is_some());
931 assert!(batch.graphs[1].graph.is_some());
932 }
933
934 #[test]
935 fn style_semantic_graph_batch_feeds_workspace_design_token_candidates() {
936 let input = sample_input();
937 let batch = summarize_omena_query_style_semantic_graph_batch_from_sources(
938 [
939 ("/tmp/tokens.module.scss", ":root { --brand: red; }"),
940 ("/tmp/theme.module.scss", "@forward \"./tokens\";"),
941 ("/tmp/unrelated.module.scss", ":root { --brand: blue; }"),
942 (
943 "/tmp/App.module.scss",
944 "@use \"./theme\";\n.button { color: var(--brand); }",
945 ),
946 ],
947 &input,
948 );
949
950 let app_graph = batch
951 .graphs
952 .iter()
953 .find(|entry| entry.style_path == "/tmp/App.module.scss")
954 .and_then(|entry| entry.graph.as_ref());
955 assert!(app_graph.is_some());
956 let Some(app_graph) = app_graph else {
957 return;
958 };
959 let design_tokens = &app_graph.design_token_semantics;
960
961 assert_eq!(
962 design_tokens.status,
963 "cross-file-import-cascade-ranking-seed"
964 );
965 assert_eq!(
966 design_tokens.resolution_scope,
967 "cross-file-import-candidate"
968 );
969 assert!(
970 design_tokens
971 .capabilities
972 .workspace_cascade_candidate_signal_ready
973 );
974 assert!(design_tokens.capabilities.cross_file_import_graph_ready);
975 assert_eq!(
976 design_tokens
977 .resolution_signal
978 .cross_file_declaration_fact_count,
979 1
980 );
981 assert_eq!(
982 design_tokens
983 .resolution_signal
984 .workspace_occurrence_resolved_reference_count,
985 1
986 );
987 assert_eq!(
988 design_tokens
989 .cascade_ranking_signal
990 .cross_file_candidate_declaration_count,
991 1
992 );
993 assert_eq!(
994 design_tokens
995 .cascade_ranking_signal
996 .cross_file_winner_declaration_count,
997 1
998 );
999 assert_eq!(
1000 design_tokens.cascade_ranking_signal.ranked_references[0]
1001 .winner_declaration_file_path
1002 .as_deref(),
1003 Some("/tmp/tokens.module.scss")
1004 );
1005 let winner_range =
1006 design_tokens.cascade_ranking_signal.ranked_references[0].winner_declaration_range;
1007 assert_eq!(winner_range.map(|range| range.start.line), Some(0));
1008 assert_eq!(winner_range.map(|range| range.start.character), Some(8));
1009 assert_eq!(
1010 design_tokens.cascade_ranking_signal.ranked_references[0]
1011 .cross_file_candidate_declaration_count,
1012 1
1013 );
1014 }
1015
1016 #[test]
1017 fn style_semantic_graph_batch_prefers_nearer_import_graph_token_candidates() {
1018 let input = sample_input();
1019 let batch = summarize_omena_query_style_semantic_graph_batch_from_sources(
1020 [
1021 ("/tmp/a-direct.module.scss", ":root { --brand: direct; }"),
1022 ("/tmp/mid.module.scss", "@forward \"./z-transitive\";"),
1023 (
1024 "/tmp/z-transitive.module.scss",
1025 ":root { --brand: transitive; }",
1026 ),
1027 (
1028 "/tmp/App.module.scss",
1029 "@use \"./a-direct\";\n@use \"./mid\";\n.button { color: var(--brand); }",
1030 ),
1031 ],
1032 &input,
1033 );
1034
1035 let app_graph = batch
1036 .graphs
1037 .iter()
1038 .find(|entry| entry.style_path == "/tmp/App.module.scss")
1039 .and_then(|entry| entry.graph.as_ref());
1040 assert!(app_graph.is_some());
1041 let Some(app_graph) = app_graph else {
1042 return;
1043 };
1044 let ranked_reference = &app_graph
1045 .design_token_semantics
1046 .cascade_ranking_signal
1047 .ranked_references[0];
1048
1049 assert_eq!(
1050 ranked_reference.winner_declaration_file_path.as_deref(),
1051 Some("/tmp/a-direct.module.scss")
1052 );
1053 assert_eq!(ranked_reference.winner_import_graph_distance, Some(1));
1054 assert_eq!(ranked_reference.winner_import_graph_order, Some(0));
1055 assert_eq!(ranked_reference.cross_file_candidate_declaration_count, 2);
1056 assert_eq!(ranked_reference.cross_file_shadowed_declaration_count, 1);
1057 }
1058
1059 #[test]
1060 fn style_semantic_graph_batch_resolves_package_root_forward_chain_token_candidates() {
1061 let input = sample_input();
1062 let batch = summarize_omena_query_style_semantic_graph_batch_from_sources(
1063 [
1064 (
1065 "/fake/workspace/node_modules/@design/tokens/src/index.scss",
1066 "@forward \"./colors\";",
1067 ),
1068 (
1069 "/fake/workspace/node_modules/@design/tokens/src/_colors.scss",
1070 ":root { --brand: package; }",
1071 ),
1072 (
1073 "/fake/workspace/src/_utils.scss",
1074 "@forward \"@design/tokens\" as ds_*;",
1075 ),
1076 (
1077 "/fake/workspace/src/App.module.scss",
1078 "@use \"./utils\";\n.button { color: var(--brand); }",
1079 ),
1080 ],
1081 &input,
1082 );
1083
1084 let app_graph = batch
1085 .graphs
1086 .iter()
1087 .find(|entry| entry.style_path == "/fake/workspace/src/App.module.scss")
1088 .and_then(|entry| entry.graph.as_ref());
1089 assert!(app_graph.is_some());
1090 let Some(app_graph) = app_graph else {
1091 return;
1092 };
1093 let ranked_reference = &app_graph
1094 .design_token_semantics
1095 .cascade_ranking_signal
1096 .ranked_references[0];
1097
1098 assert_eq!(
1099 ranked_reference.winner_declaration_file_path.as_deref(),
1100 Some("/fake/workspace/node_modules/@design/tokens/src/_colors.scss")
1101 );
1102 assert_eq!(ranked_reference.winner_import_graph_distance, Some(3));
1103 assert_eq!(ranked_reference.cross_file_candidate_declaration_count, 1);
1104 }
1105
1106 fn backend<'a>(
1107 summary: &'a SelectedQueryAdapterCapabilitiesV0,
1108 backend_kind: &str,
1109 ) -> Option<&'a super::SelectedQueryBackendCapabilityV0> {
1110 summary
1111 .backend_kinds
1112 .iter()
1113 .find(|backend| backend.backend_kind == backend_kind)
1114 }
1115
1116 fn sample_input() -> EngineInputV2 {
1117 EngineInputV2 {
1118 version: "2".to_string(),
1119 sources: vec![SourceAnalysisInputV2 {
1120 document: SourceDocumentV2 {
1121 class_expressions: vec![
1122 ClassExpressionInputV2 {
1123 id: "expr-1".to_string(),
1124 kind: "symbolRef".to_string(),
1125 scss_module_path: "/tmp/App.module.scss".to_string(),
1126 range: range(4, 12, 4, 16),
1127 class_name: None,
1128 root_binding_decl_id: Some("decl-1".to_string()),
1129 access_path: None,
1130 },
1131 ClassExpressionInputV2 {
1132 id: "expr-2".to_string(),
1133 kind: "styleAccess".to_string(),
1134 scss_module_path: "/tmp/Card.module.scss".to_string(),
1135 range: range(6, 9, 6, 20),
1136 class_name: Some("card-header".to_string()),
1137 root_binding_decl_id: None,
1138 access_path: Some(vec!["card".to_string(), "header".to_string()]),
1139 },
1140 ],
1141 },
1142 }],
1143 styles: vec![
1144 StyleAnalysisInputV2 {
1145 file_path: "/tmp/App.module.scss".to_string(),
1146 document: StyleDocumentV2 {
1147 selectors: vec![StyleSelectorV2 {
1148 name: "btn-active".to_string(),
1149 view_kind: "canonical".to_string(),
1150 canonical_name: Some("btn-active".to_string()),
1151 range: range(1, 1, 1, 12),
1152 nested_safety: Some("safe".to_string()),
1153 composes: None,
1154 bem_suffix: None,
1155 }],
1156 },
1157 },
1158 StyleAnalysisInputV2 {
1159 file_path: "/tmp/Card.module.scss".to_string(),
1160 document: StyleDocumentV2 {
1161 selectors: vec![StyleSelectorV2 {
1162 name: "card-header".to_string(),
1163 view_kind: "canonical".to_string(),
1164 canonical_name: Some("card-header".to_string()),
1165 range: range(3, 1, 3, 13),
1166 nested_safety: Some("unsafe".to_string()),
1167 composes: None,
1168 bem_suffix: None,
1169 }],
1170 },
1171 },
1172 ],
1173 type_facts: vec![
1174 TypeFactEntryV2 {
1175 file_path: "/tmp/App.tsx".to_string(),
1176 expression_id: "expr-1".to_string(),
1177 facts: StringTypeFactsV2 {
1178 kind: "constrained".to_string(),
1179 constraint_kind: Some("prefixSuffix".to_string()),
1180 values: None,
1181 prefix: Some("btn-".to_string()),
1182 suffix: Some("-active".to_string()),
1183 min_len: Some(10),
1184 max_len: None,
1185 char_must: None,
1186 char_may: None,
1187 may_include_other_chars: None,
1188 },
1189 },
1190 TypeFactEntryV2 {
1191 file_path: "/tmp/Card.tsx".to_string(),
1192 expression_id: "expr-2".to_string(),
1193 facts: StringTypeFactsV2 {
1194 kind: "finiteSet".to_string(),
1195 constraint_kind: None,
1196 values: Some(vec!["card-header".to_string(), "card-body".to_string()]),
1197 prefix: None,
1198 suffix: None,
1199 min_len: None,
1200 max_len: None,
1201 char_must: None,
1202 char_may: None,
1203 may_include_other_chars: None,
1204 },
1205 },
1206 ],
1207 }
1208 }
1209
1210 fn range(
1211 start_line: usize,
1212 start_character: usize,
1213 end_line: usize,
1214 end_character: usize,
1215 ) -> RangeV2 {
1216 RangeV2 {
1217 start: PositionV2 {
1218 line: start_line,
1219 character: start_character,
1220 },
1221 end: PositionV2 {
1222 line: end_line,
1223 character: end_character,
1224 },
1225 }
1226 }
1227}