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_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}