Skip to main content

omena_query/
lib.rs

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}