Skip to main content

omena_resolver/
lib.rs

1use std::collections::{BTreeMap, BTreeSet};
2
3use engine_input_producers::{
4    EngineInputV2, SourceResolutionCandidateV0, SourceResolutionCanonicalProducerSignalV0,
5    SourceResolutionQueryFragmentV0, SourceResolutionQueryFragmentsV0,
6    summarize_source_resolution_canonical_producer_signal_input,
7    summarize_source_resolution_query_fragments_input,
8};
9use serde::{Deserialize, Serialize};
10
11#[derive(Debug, Serialize)]
12#[serde(rename_all = "camelCase")]
13pub struct OmenaResolverBoundarySummaryV0 {
14    pub schema_version: &'static str,
15    pub product: &'static str,
16    pub resolver_name: &'static str,
17    pub input_version: String,
18    pub delegated_source_resolution_products: Vec<&'static str>,
19    pub resolver_owned_products: Vec<&'static str>,
20    pub source_resolution_query_count: usize,
21    pub source_resolution_candidate_count: usize,
22    pub source_resolution_evaluator_candidate_count: usize,
23    pub module_graph_module_count: usize,
24    pub module_graph_source_expression_edge_count: usize,
25    pub runtime_query_module_count: usize,
26    pub runtime_query_ready_module_count: usize,
27    pub source_resolution_runtime_expression_count: usize,
28    pub source_resolution_runtime_resolved_expression_count: usize,
29    pub ready_surfaces: Vec<&'static str>,
30    pub cme_coupled_surfaces: Vec<&'static str>,
31    pub next_decoupling_targets: Vec<&'static str>,
32}
33
34#[derive(Debug, Clone, PartialEq, Eq, Deserialize, Serialize)]
35#[serde(rename_all = "camelCase")]
36pub struct OmenaResolverModuleGraphSummaryV0 {
37    pub schema_version: String,
38    pub product: String,
39    pub input_version: String,
40    pub module_count: usize,
41    pub source_expression_edge_count: usize,
42    pub type_fact_edge_count: usize,
43    pub selector_count: usize,
44    pub unresolved_type_fact_count: usize,
45    pub modules: Vec<OmenaResolverModuleGraphModuleV0>,
46    pub unresolved_type_fact_expression_ids: Vec<String>,
47}
48
49#[derive(Debug, Clone, PartialEq, Eq, Deserialize, Serialize)]
50#[serde(rename_all = "camelCase")]
51pub struct OmenaResolverModuleGraphModuleV0 {
52    pub style_file_path: String,
53    pub source_expression_ids: Vec<String>,
54    pub source_expression_kinds: Vec<String>,
55    pub type_fact_expression_ids: Vec<String>,
56    pub selector_names: Vec<String>,
57    pub canonical_selector_names: Vec<String>,
58    pub has_source_input: bool,
59    pub has_style_input: bool,
60    pub has_type_fact_input: bool,
61}
62
63#[derive(Debug, Clone, PartialEq, Eq, Serialize)]
64#[serde(rename_all = "camelCase")]
65pub struct OmenaResolverRuntimeQueryBoundarySummaryV0 {
66    pub schema_version: &'static str,
67    pub product: &'static str,
68    pub input_product: String,
69    pub input_version: String,
70    pub module_query_count: usize,
71    pub fully_resolvable_module_count: usize,
72    pub source_only_module_count: usize,
73    pub style_only_module_count: usize,
74    pub unresolved_type_fact_count: usize,
75    pub runtime_capabilities: Vec<&'static str>,
76    pub blocking_gaps: Vec<&'static str>,
77    pub module_queries: Vec<OmenaResolverRuntimeModuleQueryV0>,
78}
79
80#[derive(Debug, Clone, PartialEq, Eq, Serialize)]
81#[serde(rename_all = "camelCase")]
82pub struct OmenaResolverRuntimeModuleQueryV0 {
83    pub style_file_path: String,
84    pub source_expression_ids: Vec<String>,
85    pub type_fact_expression_ids: Vec<String>,
86    pub selector_names: Vec<String>,
87    pub canonical_selector_names: Vec<String>,
88    pub can_resolve_source_expressions: bool,
89    pub can_check_type_fact_edges: bool,
90    pub can_query_selector_names: bool,
91    pub status: &'static str,
92}
93
94#[derive(Debug, Clone, PartialEq, Eq, Serialize)]
95#[serde(rename_all = "camelCase")]
96pub struct OmenaResolverSourceResolutionRuntimeIndexV0 {
97    pub schema_version: &'static str,
98    pub product: &'static str,
99    pub input_product: &'static str,
100    pub input_version: String,
101    pub expression_count: usize,
102    pub resolved_expression_count: usize,
103    pub unresolved_expression_count: usize,
104    pub blocking_gaps: Vec<&'static str>,
105    pub entries: Vec<OmenaResolverSourceResolutionRuntimeEntryV0>,
106}
107
108#[derive(Debug, Clone, PartialEq, Eq, Serialize)]
109#[serde(rename_all = "camelCase")]
110pub struct OmenaResolverSourceResolutionRuntimeEntryV0 {
111    pub query_id: String,
112    pub expression_id: String,
113    pub expression_kind: String,
114    pub style_file_path: String,
115    pub selector_names: Vec<String>,
116    pub finite_values: Option<Vec<String>>,
117    pub selector_certainty: String,
118    pub value_certainty: Option<String>,
119    pub selector_certainty_shape_kind: String,
120    pub value_certainty_shape_kind: String,
121    pub has_selector_match: bool,
122    pub has_finite_values: bool,
123    pub can_resolve_source_expression: bool,
124    pub status: &'static str,
125}
126
127#[derive(Debug, Default)]
128struct ModuleGraphAccumulator {
129    source_expression_ids: BTreeSet<String>,
130    source_expression_kinds: BTreeSet<String>,
131    type_fact_expression_ids: BTreeSet<String>,
132    selector_names: BTreeSet<String>,
133    canonical_selector_names: BTreeSet<String>,
134    has_source_input: bool,
135    has_style_input: bool,
136    has_type_fact_input: bool,
137}
138
139pub fn summarize_omena_resolver_boundary(input: &EngineInputV2) -> OmenaResolverBoundarySummaryV0 {
140    let canonical_signal = summarize_omena_resolver_canonical_producer_signal(input);
141    let module_graph = summarize_omena_resolver_module_graph_index(input);
142    let runtime_query = summarize_omena_resolver_runtime_query_boundary(&module_graph);
143    let source_resolution_runtime = summarize_omena_resolver_source_resolution_runtime(input);
144
145    OmenaResolverBoundarySummaryV0 {
146        schema_version: "0",
147        product: "omena-resolver.boundary",
148        resolver_name: "omena-resolver",
149        input_version: input.version.clone(),
150        delegated_source_resolution_products: vec![
151            "engine-input-producers.source-resolution-query-fragments",
152            "engine-input-producers.source-resolution-canonical-producer",
153        ],
154        resolver_owned_products: vec![
155            "omena-resolver.module-graph-index",
156            "omena-resolver.runtime-query-boundary",
157            "omena-resolver.source-resolution-runtime-index",
158        ],
159        source_resolution_query_count: canonical_signal.canonical_bundle.query_fragments.len(),
160        source_resolution_candidate_count: canonical_signal.canonical_bundle.candidates.len(),
161        source_resolution_evaluator_candidate_count: canonical_signal
162            .evaluator_candidates
163            .results
164            .len(),
165        module_graph_module_count: module_graph.module_count,
166        module_graph_source_expression_edge_count: module_graph.source_expression_edge_count,
167        runtime_query_module_count: runtime_query.module_query_count,
168        runtime_query_ready_module_count: runtime_query.fully_resolvable_module_count,
169        source_resolution_runtime_expression_count: source_resolution_runtime.expression_count,
170        source_resolution_runtime_resolved_expression_count: source_resolution_runtime
171            .resolved_expression_count,
172        ready_surfaces: vec![
173            "resolverBoundarySummary",
174            "resolverModuleGraphIndex",
175            "resolverRuntimeQueryBoundary",
176            "resolverSourceResolutionRuntimeIndex",
177            "sourceResolutionQueryFragments",
178            "sourceResolutionCanonicalProducerSignal",
179        ],
180        cme_coupled_surfaces: vec!["EngineInputV2", "producerSourceResolutionRows"],
181        next_decoupling_targets: vec!["specifierResolutionRuntime", "tsconfigPathMapping"],
182    }
183}
184
185pub fn summarize_omena_resolver_module_graph_index(
186    input: &EngineInputV2,
187) -> OmenaResolverModuleGraphSummaryV0 {
188    let mut modules = BTreeMap::<String, ModuleGraphAccumulator>::new();
189    let mut expression_to_style_path = BTreeMap::<String, String>::new();
190    let mut source_expression_edge_count = 0usize;
191    let mut type_fact_edge_count = 0usize;
192    let mut selector_count = 0usize;
193    let mut unresolved_type_fact_expression_ids = BTreeSet::<String>::new();
194
195    for source in &input.sources {
196        for expression in &source.document.class_expressions {
197            source_expression_edge_count += 1;
198            expression_to_style_path
199                .insert(expression.id.clone(), expression.scss_module_path.clone());
200            let module = modules
201                .entry(expression.scss_module_path.clone())
202                .or_default();
203            module.has_source_input = true;
204            module.source_expression_ids.insert(expression.id.clone());
205            module
206                .source_expression_kinds
207                .insert(expression.kind.clone());
208        }
209    }
210
211    for style in &input.styles {
212        let module = modules.entry(style.file_path.clone()).or_default();
213        module.has_style_input = true;
214        for selector in &style.document.selectors {
215            selector_count += 1;
216            module.selector_names.insert(selector.name.clone());
217            if let Some(canonical_name) = &selector.canonical_name {
218                module
219                    .canonical_selector_names
220                    .insert(canonical_name.clone());
221            }
222        }
223    }
224
225    for type_fact in &input.type_facts {
226        if let Some(style_file_path) = expression_to_style_path.get(&type_fact.expression_id) {
227            type_fact_edge_count += 1;
228            let module = modules.entry(style_file_path.clone()).or_default();
229            module.has_type_fact_input = true;
230            module
231                .type_fact_expression_ids
232                .insert(type_fact.expression_id.clone());
233        } else {
234            unresolved_type_fact_expression_ids.insert(type_fact.expression_id.clone());
235        }
236    }
237
238    let modules = modules
239        .into_iter()
240        .map(
241            |(style_file_path, module)| OmenaResolverModuleGraphModuleV0 {
242                style_file_path,
243                source_expression_ids: module.source_expression_ids.into_iter().collect(),
244                source_expression_kinds: module.source_expression_kinds.into_iter().collect(),
245                type_fact_expression_ids: module.type_fact_expression_ids.into_iter().collect(),
246                selector_names: module.selector_names.into_iter().collect(),
247                canonical_selector_names: module.canonical_selector_names.into_iter().collect(),
248                has_source_input: module.has_source_input,
249                has_style_input: module.has_style_input,
250                has_type_fact_input: module.has_type_fact_input,
251            },
252        )
253        .collect::<Vec<_>>();
254    let unresolved_type_fact_expression_ids = unresolved_type_fact_expression_ids
255        .into_iter()
256        .collect::<Vec<_>>();
257
258    OmenaResolverModuleGraphSummaryV0 {
259        schema_version: "0".to_string(),
260        product: "omena-resolver.module-graph-index".to_string(),
261        input_version: input.version.clone(),
262        module_count: modules.len(),
263        source_expression_edge_count,
264        type_fact_edge_count,
265        selector_count,
266        unresolved_type_fact_count: unresolved_type_fact_expression_ids.len(),
267        modules,
268        unresolved_type_fact_expression_ids,
269    }
270}
271
272pub fn summarize_omena_resolver_runtime_query_boundary(
273    module_graph: &OmenaResolverModuleGraphSummaryV0,
274) -> OmenaResolverRuntimeQueryBoundarySummaryV0 {
275    let module_queries = module_graph
276        .modules
277        .iter()
278        .map(runtime_module_query_from_graph_module)
279        .collect::<Vec<_>>();
280    let fully_resolvable_module_count = module_queries
281        .iter()
282        .filter(|module| module.status == "ready")
283        .count();
284    let source_only_module_count = module_graph
285        .modules
286        .iter()
287        .filter(|module| module.has_source_input && !module.has_style_input)
288        .count();
289    let style_only_module_count = module_graph
290        .modules
291        .iter()
292        .filter(|module| module.has_style_input && !module.has_source_input)
293        .count();
294    let mut blocking_gaps = Vec::new();
295
296    if module_graph.module_count == 0 {
297        blocking_gaps.push("emptyModuleGraph");
298    }
299    if fully_resolvable_module_count < module_graph.module_count {
300        blocking_gaps.push("partialModuleCoverage");
301    }
302    if module_graph.unresolved_type_fact_count > 0 {
303        blocking_gaps.push("unresolvedTypeFactEdges");
304    }
305
306    OmenaResolverRuntimeQueryBoundarySummaryV0 {
307        schema_version: "0",
308        product: "omena-resolver.runtime-query-boundary",
309        input_product: module_graph.product.clone(),
310        input_version: module_graph.input_version.clone(),
311        module_query_count: module_queries.len(),
312        fully_resolvable_module_count,
313        source_only_module_count,
314        style_only_module_count,
315        unresolved_type_fact_count: module_graph.unresolved_type_fact_count,
316        runtime_capabilities: vec![
317            "moduleLookupByStylePath",
318            "sourceExpressionEdgeLookup",
319            "typeFactEdgeLookup",
320            "selectorNameLookup",
321        ],
322        blocking_gaps,
323        module_queries,
324    }
325}
326
327pub fn query_omena_resolver_runtime_module(
328    module_graph: &OmenaResolverModuleGraphSummaryV0,
329    style_file_path: &str,
330) -> Option<OmenaResolverRuntimeModuleQueryV0> {
331    module_graph
332        .modules
333        .iter()
334        .find(|module| module.style_file_path == style_file_path)
335        .map(runtime_module_query_from_graph_module)
336}
337
338fn runtime_module_query_from_graph_module(
339    module: &OmenaResolverModuleGraphModuleV0,
340) -> OmenaResolverRuntimeModuleQueryV0 {
341    OmenaResolverRuntimeModuleQueryV0 {
342        style_file_path: module.style_file_path.clone(),
343        source_expression_ids: module.source_expression_ids.clone(),
344        type_fact_expression_ids: module.type_fact_expression_ids.clone(),
345        selector_names: module.selector_names.clone(),
346        canonical_selector_names: module.canonical_selector_names.clone(),
347        can_resolve_source_expressions: module.has_source_input && module.has_style_input,
348        can_check_type_fact_edges: module.has_source_input && module.has_type_fact_input,
349        can_query_selector_names: module.has_style_input,
350        status: module_runtime_status(module),
351    }
352}
353
354fn module_runtime_status(module: &OmenaResolverModuleGraphModuleV0) -> &'static str {
355    if module.has_source_input && module.has_style_input && module.has_type_fact_input {
356        "ready"
357    } else if module.has_source_input && !module.has_style_input {
358        "sourceOnly"
359    } else if module.has_style_input && !module.has_source_input {
360        "styleOnly"
361    } else {
362        "partial"
363    }
364}
365
366pub fn summarize_omena_resolver_query_fragments(
367    input: &EngineInputV2,
368) -> SourceResolutionQueryFragmentsV0 {
369    summarize_source_resolution_query_fragments_input(input)
370}
371
372pub fn summarize_omena_resolver_canonical_producer_signal(
373    input: &EngineInputV2,
374) -> SourceResolutionCanonicalProducerSignalV0 {
375    summarize_source_resolution_canonical_producer_signal_input(input)
376}
377
378pub fn summarize_omena_resolver_source_resolution_runtime(
379    input: &EngineInputV2,
380) -> OmenaResolverSourceResolutionRuntimeIndexV0 {
381    let canonical_signal = summarize_omena_resolver_canonical_producer_signal(input);
382    let mut candidates_by_expression = BTreeMap::<String, SourceResolutionCandidateV0>::new();
383
384    for candidate in canonical_signal.canonical_bundle.candidates {
385        candidates_by_expression.insert(candidate.expression_id.clone(), candidate);
386    }
387
388    let entries = canonical_signal
389        .canonical_bundle
390        .query_fragments
391        .iter()
392        .map(|fragment| {
393            runtime_source_resolution_entry_from_fragment(
394                fragment,
395                candidates_by_expression.get(&fragment.expression_id),
396            )
397        })
398        .collect::<Vec<_>>();
399    let resolved_expression_count = entries
400        .iter()
401        .filter(|entry| entry.can_resolve_source_expression)
402        .count();
403    let unresolved_expression_count = entries.len() - resolved_expression_count;
404    let mut blocking_gaps = Vec::new();
405
406    if entries.is_empty() {
407        blocking_gaps.push("emptySourceResolutionRuntimeIndex");
408    }
409    if unresolved_expression_count > 0 {
410        blocking_gaps.push("unresolvedSourceExpressions");
411    }
412
413    OmenaResolverSourceResolutionRuntimeIndexV0 {
414        schema_version: "0",
415        product: "omena-resolver.source-resolution-runtime-index",
416        input_product: "engine-input-producers.source-resolution-canonical-producer",
417        input_version: canonical_signal.input_version,
418        expression_count: entries.len(),
419        resolved_expression_count,
420        unresolved_expression_count,
421        blocking_gaps,
422        entries,
423    }
424}
425
426pub fn query_omena_resolver_source_expression(
427    runtime_index: &OmenaResolverSourceResolutionRuntimeIndexV0,
428    expression_id: &str,
429) -> Option<OmenaResolverSourceResolutionRuntimeEntryV0> {
430    runtime_index
431        .entries
432        .iter()
433        .find(|entry| entry.expression_id == expression_id)
434        .cloned()
435}
436
437fn runtime_source_resolution_entry_from_fragment(
438    fragment: &SourceResolutionQueryFragmentV0,
439    candidate: Option<&SourceResolutionCandidateV0>,
440) -> OmenaResolverSourceResolutionRuntimeEntryV0 {
441    let selector_names = candidate
442        .map(|candidate| candidate.selector_names.clone())
443        .unwrap_or_default();
444    let finite_values = candidate.and_then(|candidate| candidate.finite_values.clone());
445    let has_selector_match = !selector_names.is_empty();
446    let has_finite_values = finite_values
447        .as_ref()
448        .is_some_and(|values| !values.is_empty());
449
450    OmenaResolverSourceResolutionRuntimeEntryV0 {
451        query_id: fragment.query_id.clone(),
452        expression_id: fragment.expression_id.clone(),
453        expression_kind: fragment.expression_kind.clone(),
454        style_file_path: fragment.style_file_path.clone(),
455        selector_names,
456        finite_values,
457        selector_certainty: candidate
458            .map(|candidate| candidate.selector_certainty.clone())
459            .unwrap_or_else(|| "unresolved".to_string()),
460        value_certainty: candidate.and_then(|candidate| candidate.value_certainty.clone()),
461        selector_certainty_shape_kind: candidate
462            .map(|candidate| candidate.selector_certainty_shape_kind.clone())
463            .unwrap_or_else(|| "missingTypeFacts".to_string()),
464        value_certainty_shape_kind: candidate
465            .map(|candidate| candidate.value_certainty_shape_kind.clone())
466            .unwrap_or_else(|| "missingTypeFacts".to_string()),
467        has_selector_match,
468        has_finite_values,
469        can_resolve_source_expression: has_selector_match,
470        status: if has_selector_match {
471            "resolved"
472        } else if candidate.is_some() {
473            "unresolvedSelectorSet"
474        } else {
475            "missingTypeFacts"
476        },
477    }
478}
479
480#[cfg(test)]
481mod tests {
482    use engine_input_producers::{
483        ClassExpressionInputV2, EngineInputV2, PositionV2, RangeV2, SourceAnalysisInputV2,
484        SourceDocumentV2, StringTypeFactsV2, StyleAnalysisInputV2, StyleDocumentV2,
485        StyleSelectorV2, TypeFactEntryV2,
486    };
487
488    use super::{
489        query_omena_resolver_runtime_module, query_omena_resolver_source_expression,
490        summarize_omena_resolver_boundary, summarize_omena_resolver_canonical_producer_signal,
491        summarize_omena_resolver_module_graph_index, summarize_omena_resolver_query_fragments,
492        summarize_omena_resolver_runtime_query_boundary,
493        summarize_omena_resolver_source_resolution_runtime,
494    };
495
496    #[test]
497    fn summarizes_resolver_boundary_over_source_resolution_products() {
498        let input = sample_input();
499        let summary = summarize_omena_resolver_boundary(&input);
500
501        assert_eq!(summary.schema_version, "0");
502        assert_eq!(summary.product, "omena-resolver.boundary");
503        assert_eq!(summary.resolver_name, "omena-resolver");
504        assert_eq!(summary.input_version, "2");
505        assert_eq!(summary.source_resolution_query_count, 2);
506        assert_eq!(summary.source_resolution_candidate_count, 2);
507        assert_eq!(summary.source_resolution_evaluator_candidate_count, 2);
508        assert_eq!(summary.module_graph_module_count, 2);
509        assert_eq!(summary.module_graph_source_expression_edge_count, 2);
510        assert_eq!(summary.runtime_query_module_count, 2);
511        assert_eq!(summary.runtime_query_ready_module_count, 2);
512        assert_eq!(summary.source_resolution_runtime_expression_count, 2);
513        assert_eq!(
514            summary.source_resolution_runtime_resolved_expression_count,
515            2
516        );
517        assert!(
518            summary
519                .delegated_source_resolution_products
520                .contains(&"engine-input-producers.source-resolution-canonical-producer")
521        );
522        assert!(
523            summary
524                .resolver_owned_products
525                .contains(&"omena-resolver.module-graph-index")
526        );
527        assert!(
528            summary
529                .resolver_owned_products
530                .contains(&"omena-resolver.runtime-query-boundary")
531        );
532        assert!(
533            summary
534                .resolver_owned_products
535                .contains(&"omena-resolver.source-resolution-runtime-index")
536        );
537        assert!(summary.ready_surfaces.contains(&"resolverModuleGraphIndex"));
538        assert!(
539            summary
540                .ready_surfaces
541                .contains(&"resolverRuntimeQueryBoundary")
542        );
543        assert!(
544            summary
545                .ready_surfaces
546                .contains(&"resolverSourceResolutionRuntimeIndex")
547        );
548        assert!(
549            summary
550                .next_decoupling_targets
551                .contains(&"tsconfigPathMapping")
552        );
553    }
554
555    #[test]
556    fn builds_resolver_module_graph_index_from_engine_input() {
557        let input = sample_input();
558        let summary = summarize_omena_resolver_module_graph_index(&input);
559
560        assert_eq!(summary.schema_version, "0");
561        assert_eq!(summary.product, "omena-resolver.module-graph-index");
562        assert_eq!(summary.input_version, "2");
563        assert_eq!(summary.module_count, 2);
564        assert_eq!(summary.source_expression_edge_count, 2);
565        assert_eq!(summary.type_fact_edge_count, 2);
566        assert_eq!(summary.selector_count, 2);
567        assert_eq!(summary.unresolved_type_fact_count, 0);
568        assert!(summary.unresolved_type_fact_expression_ids.is_empty());
569
570        let app = summary
571            .modules
572            .iter()
573            .find(|module| module.style_file_path == "/tmp/App.module.scss");
574        assert!(app.is_some());
575        let Some(app) = app else {
576            return;
577        };
578        assert_eq!(app.source_expression_ids, ["expr-1"]);
579        assert_eq!(app.source_expression_kinds, ["symbolRef"]);
580        assert_eq!(app.type_fact_expression_ids, ["expr-1"]);
581        assert_eq!(app.selector_names, ["btn-active"]);
582        assert_eq!(app.canonical_selector_names, ["btn-active"]);
583        assert!(app.has_source_input);
584        assert!(app.has_style_input);
585        assert!(app.has_type_fact_input);
586
587        let card = summary
588            .modules
589            .iter()
590            .find(|module| module.style_file_path == "/tmp/Card.module.scss");
591        assert!(card.is_some());
592        let Some(card) = card else {
593            return;
594        };
595        assert_eq!(card.source_expression_ids, ["expr-2"]);
596        assert_eq!(card.source_expression_kinds, ["styleAccess"]);
597        assert_eq!(card.type_fact_expression_ids, ["expr-2"]);
598        assert_eq!(card.selector_names, ["card-header"]);
599        assert_eq!(card.canonical_selector_names, ["card-header"]);
600    }
601
602    #[test]
603    fn exposes_runtime_query_boundary_from_module_graph_index() {
604        let input = sample_input();
605        let module_graph = summarize_omena_resolver_module_graph_index(&input);
606        let runtime_query = summarize_omena_resolver_runtime_query_boundary(&module_graph);
607
608        assert_eq!(runtime_query.schema_version, "0");
609        assert_eq!(
610            runtime_query.product,
611            "omena-resolver.runtime-query-boundary"
612        );
613        assert_eq!(
614            runtime_query.input_product,
615            "omena-resolver.module-graph-index"
616        );
617        assert_eq!(runtime_query.input_version, "2");
618        assert_eq!(runtime_query.module_query_count, 2);
619        assert_eq!(runtime_query.fully_resolvable_module_count, 2);
620        assert_eq!(runtime_query.source_only_module_count, 0);
621        assert_eq!(runtime_query.style_only_module_count, 0);
622        assert_eq!(runtime_query.unresolved_type_fact_count, 0);
623        assert!(runtime_query.blocking_gaps.is_empty());
624        assert!(
625            runtime_query
626                .runtime_capabilities
627                .contains(&"moduleLookupByStylePath")
628        );
629
630        let app = query_omena_resolver_runtime_module(&module_graph, "/tmp/App.module.scss");
631        assert!(app.is_some());
632        let Some(app) = app else {
633            return;
634        };
635        assert_eq!(app.status, "ready");
636        assert!(app.can_resolve_source_expressions);
637        assert!(app.can_check_type_fact_edges);
638        assert!(app.can_query_selector_names);
639        assert_eq!(app.source_expression_ids, ["expr-1"]);
640        assert_eq!(app.selector_names, ["btn-active"]);
641    }
642
643    #[test]
644    fn builds_source_resolution_runtime_index_from_canonical_candidates() {
645        let input = sample_input();
646        let runtime_index = summarize_omena_resolver_source_resolution_runtime(&input);
647
648        assert_eq!(runtime_index.schema_version, "0");
649        assert_eq!(
650            runtime_index.product,
651            "omena-resolver.source-resolution-runtime-index"
652        );
653        assert_eq!(
654            runtime_index.input_product,
655            "engine-input-producers.source-resolution-canonical-producer"
656        );
657        assert_eq!(runtime_index.input_version, "2");
658        assert_eq!(runtime_index.expression_count, 2);
659        assert_eq!(runtime_index.resolved_expression_count, 2);
660        assert_eq!(runtime_index.unresolved_expression_count, 0);
661        assert!(runtime_index.blocking_gaps.is_empty());
662
663        let app = query_omena_resolver_source_expression(&runtime_index, "expr-1");
664        assert!(app.is_some());
665        let Some(app) = app else {
666            return;
667        };
668        assert_eq!(app.query_id, "expr-1");
669        assert_eq!(app.expression_kind, "symbolRef");
670        assert_eq!(app.style_file_path, "/tmp/App.module.scss");
671        assert_eq!(app.selector_names, ["btn-active"]);
672        assert_eq!(app.selector_certainty, "exact");
673        assert_eq!(app.selector_certainty_shape_kind, "exact");
674        assert_eq!(app.value_certainty_shape_kind, "constrained");
675        assert!(app.has_selector_match);
676        assert!(!app.has_finite_values);
677        assert!(app.can_resolve_source_expression);
678        assert_eq!(app.status, "resolved");
679
680        let card = query_omena_resolver_source_expression(&runtime_index, "expr-2");
681        assert!(card.is_some());
682        let Some(card) = card else {
683            return;
684        };
685        assert_eq!(card.selector_names, ["card-header"]);
686        assert_eq!(
687            card.finite_values,
688            Some(vec!["card-header".to_string(), "card-body".to_string()])
689        );
690        assert!(card.has_finite_values);
691    }
692
693    #[test]
694    fn exposes_stable_query_fragment_and_canonical_producer_wrappers() {
695        let input = sample_input();
696
697        let query_fragments = summarize_omena_resolver_query_fragments(&input);
698        assert_eq!(query_fragments.schema_version, "0");
699        assert_eq!(query_fragments.input_version, "2");
700        assert_eq!(query_fragments.fragments.len(), 2);
701        assert_eq!(query_fragments.fragments[0].query_id, "expr-1");
702        assert_eq!(
703            query_fragments.fragments[1].style_file_path,
704            "/tmp/Card.module.scss"
705        );
706
707        let canonical_signal = summarize_omena_resolver_canonical_producer_signal(&input);
708        assert_eq!(canonical_signal.schema_version, "0");
709        assert_eq!(canonical_signal.input_version, "2");
710        assert_eq!(canonical_signal.canonical_bundle.query_fragments.len(), 2);
711        assert_eq!(canonical_signal.canonical_bundle.candidates.len(), 2);
712        assert_eq!(canonical_signal.evaluator_candidates.results.len(), 2);
713    }
714
715    fn sample_input() -> EngineInputV2 {
716        EngineInputV2 {
717            version: "2".to_string(),
718            sources: vec![SourceAnalysisInputV2 {
719                document: SourceDocumentV2 {
720                    class_expressions: vec![
721                        ClassExpressionInputV2 {
722                            id: "expr-1".to_string(),
723                            kind: "symbolRef".to_string(),
724                            scss_module_path: "/tmp/App.module.scss".to_string(),
725                            range: range(4, 12, 4, 16),
726                            class_name: None,
727                            root_binding_decl_id: Some("decl-1".to_string()),
728                            access_path: None,
729                        },
730                        ClassExpressionInputV2 {
731                            id: "expr-2".to_string(),
732                            kind: "styleAccess".to_string(),
733                            scss_module_path: "/tmp/Card.module.scss".to_string(),
734                            range: range(6, 9, 6, 20),
735                            class_name: Some("card-header".to_string()),
736                            root_binding_decl_id: None,
737                            access_path: Some(vec!["card".to_string(), "header".to_string()]),
738                        },
739                    ],
740                },
741            }],
742            styles: vec![
743                StyleAnalysisInputV2 {
744                    file_path: "/tmp/App.module.scss".to_string(),
745                    document: StyleDocumentV2 {
746                        selectors: vec![StyleSelectorV2 {
747                            name: "btn-active".to_string(),
748                            view_kind: "canonical".to_string(),
749                            canonical_name: Some("btn-active".to_string()),
750                            range: range(1, 1, 1, 12),
751                            nested_safety: Some("safe".to_string()),
752                            composes: None,
753                            bem_suffix: None,
754                        }],
755                    },
756                },
757                StyleAnalysisInputV2 {
758                    file_path: "/tmp/Card.module.scss".to_string(),
759                    document: StyleDocumentV2 {
760                        selectors: vec![StyleSelectorV2 {
761                            name: "card-header".to_string(),
762                            view_kind: "canonical".to_string(),
763                            canonical_name: Some("card-header".to_string()),
764                            range: range(3, 1, 3, 13),
765                            nested_safety: Some("unsafe".to_string()),
766                            composes: None,
767                            bem_suffix: None,
768                        }],
769                    },
770                },
771            ],
772            type_facts: vec![
773                TypeFactEntryV2 {
774                    file_path: "/tmp/App.tsx".to_string(),
775                    expression_id: "expr-1".to_string(),
776                    facts: StringTypeFactsV2 {
777                        kind: "constrained".to_string(),
778                        constraint_kind: Some("prefixSuffix".to_string()),
779                        values: None,
780                        prefix: Some("btn-".to_string()),
781                        suffix: Some("-active".to_string()),
782                        min_len: Some(10),
783                        max_len: None,
784                        char_must: None,
785                        char_may: None,
786                        may_include_other_chars: None,
787                    },
788                },
789                TypeFactEntryV2 {
790                    file_path: "/tmp/Card.tsx".to_string(),
791                    expression_id: "expr-2".to_string(),
792                    facts: StringTypeFactsV2 {
793                        kind: "finiteSet".to_string(),
794                        constraint_kind: None,
795                        values: Some(vec!["card-header".to_string(), "card-body".to_string()]),
796                        prefix: None,
797                        suffix: None,
798                        min_len: None,
799                        max_len: None,
800                        char_must: None,
801                        char_may: None,
802                        may_include_other_chars: None,
803                    },
804                },
805            ],
806        }
807    }
808
809    fn range(
810        start_line: usize,
811        start_character: usize,
812        end_line: usize,
813        end_character: usize,
814    ) -> RangeV2 {
815        RangeV2 {
816            start: PositionV2 {
817                line: start_line,
818                character: start_character,
819            },
820            end: PositionV2 {
821                line: end_line,
822                character: end_character,
823            },
824        }
825    }
826}