Skip to main content

omena_query/
lib.rs

1use engine_input_producers::{
2    EngineInputV2, ExpressionSemanticsCanonicalProducerSignalV0,
3    ExpressionSemanticsQueryFragmentsV0, SelectorUsageCanonicalProducerSignalV0,
4    SelectorUsageQueryFragmentsV0, SourceResolutionCanonicalProducerSignalV0,
5    SourceResolutionQueryFragmentsV0,
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 omena_abstract_value::{AbstractValueDomainSummaryV0, summarize_omena_abstract_value_domain};
12use omena_bridge::{
13    StyleSemanticGraphSummaryV0, summarize_omena_bridge_style_semantic_graph_from_source,
14};
15use omena_resolver::{
16    summarize_omena_resolver_canonical_producer_signal, summarize_omena_resolver_query_fragments,
17};
18use serde::Serialize;
19
20#[derive(Debug, Serialize)]
21#[serde(rename_all = "camelCase")]
22pub struct OmenaQueryBoundarySummaryV0 {
23    pub schema_version: &'static str,
24    pub product: &'static str,
25    pub query_engine_name: &'static str,
26    pub input_version: String,
27    pub abstract_value_domain: AbstractValueDomainSummaryV0,
28    pub selected_query_adapter_capabilities: SelectedQueryAdapterCapabilitiesV0,
29    pub delegated_fragment_products: Vec<&'static str>,
30    pub expression_semantics_query_count: usize,
31    pub source_resolution_query_count: usize,
32    pub selector_usage_query_count: usize,
33    pub total_query_count: usize,
34    pub ready_surfaces: Vec<&'static str>,
35    pub cme_coupled_surfaces: Vec<&'static str>,
36    pub next_decoupling_targets: Vec<&'static str>,
37}
38
39#[derive(Debug, Serialize)]
40#[serde(rename_all = "camelCase")]
41pub struct OmenaQueryFragmentBundleV0 {
42    pub schema_version: &'static str,
43    pub product: &'static str,
44    pub input_version: String,
45    pub expression_semantics: ExpressionSemanticsQueryFragmentsV0,
46    pub source_resolution: SourceResolutionQueryFragmentsV0,
47    pub selector_usage: SelectorUsageQueryFragmentsV0,
48}
49
50#[derive(Debug, Clone, PartialEq, Eq, Serialize)]
51#[serde(rename_all = "camelCase")]
52pub struct SelectedQueryAdapterCapabilitiesV0 {
53    pub schema_version: &'static str,
54    pub product: &'static str,
55    pub default_candidate_backend: &'static str,
56    pub backend_kinds: Vec<SelectedQueryBackendCapabilityV0>,
57    pub runner_commands: Vec<SelectedQueryRunnerCommandV0>,
58    pub expression_semantics_payload_contracts: Vec<&'static str>,
59    pub required_input_contracts: Vec<&'static str>,
60    pub adapter_readiness: Vec<&'static str>,
61    pub routing_status: &'static str,
62}
63
64#[derive(Debug, Clone, PartialEq, Eq, Serialize)]
65#[serde(rename_all = "camelCase")]
66pub struct SelectedQueryBackendCapabilityV0 {
67    pub backend_kind: &'static str,
68    pub source_resolution: bool,
69    pub expression_semantics: bool,
70    pub selector_usage: bool,
71    pub style_semantic_graph: bool,
72}
73
74#[derive(Debug, Clone, PartialEq, Eq, Serialize)]
75#[serde(rename_all = "camelCase")]
76pub struct SelectedQueryRunnerCommandV0 {
77    pub surface: &'static str,
78    pub command: &'static str,
79    pub input_contract: &'static str,
80    pub output_product: &'static str,
81}
82
83#[derive(Debug, Serialize)]
84#[serde(rename_all = "camelCase")]
85pub struct OmenaQueryStyleSemanticGraphBatchOutputV0 {
86    pub schema_version: &'static str,
87    pub product: &'static str,
88    pub graphs: Vec<OmenaQueryStyleSemanticGraphBatchEntryV0>,
89}
90
91#[derive(Debug, Serialize)]
92#[serde(rename_all = "camelCase")]
93pub struct OmenaQueryStyleSemanticGraphBatchEntryV0 {
94    pub style_path: String,
95    pub graph: Option<StyleSemanticGraphSummaryV0>,
96}
97
98pub fn summarize_omena_query_boundary(input: &EngineInputV2) -> OmenaQueryBoundarySummaryV0 {
99    let fragment_bundle = summarize_omena_query_fragment_bundle(input);
100    let expression_semantics_query_count = fragment_bundle.expression_semantics.fragments.len();
101    let source_resolution_query_count = fragment_bundle.source_resolution.fragments.len();
102    let selector_usage_query_count = fragment_bundle.selector_usage.fragments.len();
103
104    OmenaQueryBoundarySummaryV0 {
105        schema_version: "0",
106        product: "omena-query.boundary",
107        query_engine_name: "omena-query",
108        input_version: input.version.clone(),
109        abstract_value_domain: summarize_omena_abstract_value_domain(),
110        selected_query_adapter_capabilities:
111            summarize_omena_query_selected_query_adapter_capabilities(),
112        delegated_fragment_products: vec![
113            "engine-input-producers.expression-semantics-query-fragments",
114            "engine-input-producers.source-resolution-query-fragments",
115            "omena-resolver.boundary",
116            "engine-input-producers.selector-usage-query-fragments",
117        ],
118        expression_semantics_query_count,
119        source_resolution_query_count,
120        selector_usage_query_count,
121        total_query_count: expression_semantics_query_count
122            + source_resolution_query_count
123            + selector_usage_query_count,
124        ready_surfaces: vec![
125            "queryFragmentBundle",
126            "abstractValueProjectionContract",
127            "sourceResolutionResolverBoundary",
128            "queryBoundarySummary",
129        ],
130        cme_coupled_surfaces: vec!["EngineInputV2", "producerQueryFragments"],
131        next_decoupling_targets: vec!["queryEvaluationRuntime", "selectedQueryBackendAdapter"],
132    }
133}
134
135pub fn summarize_omena_query_selected_query_adapter_capabilities()
136-> SelectedQueryAdapterCapabilitiesV0 {
137    SelectedQueryAdapterCapabilitiesV0 {
138        schema_version: "0",
139        product: "omena-query.selected-query-adapter-capabilities",
140        default_candidate_backend: "rust-selected-query",
141        backend_kinds: vec![
142            SelectedQueryBackendCapabilityV0 {
143                backend_kind: "typescript-current",
144                source_resolution: false,
145                expression_semantics: false,
146                selector_usage: false,
147                style_semantic_graph: false,
148            },
149            SelectedQueryBackendCapabilityV0 {
150                backend_kind: "rust-source-resolution",
151                source_resolution: true,
152                expression_semantics: false,
153                selector_usage: false,
154                style_semantic_graph: false,
155            },
156            SelectedQueryBackendCapabilityV0 {
157                backend_kind: "rust-expression-semantics",
158                source_resolution: false,
159                expression_semantics: true,
160                selector_usage: false,
161                style_semantic_graph: false,
162            },
163            SelectedQueryBackendCapabilityV0 {
164                backend_kind: "rust-selector-usage",
165                source_resolution: false,
166                expression_semantics: false,
167                selector_usage: true,
168                style_semantic_graph: false,
169            },
170            SelectedQueryBackendCapabilityV0 {
171                backend_kind: "rust-selected-query",
172                source_resolution: true,
173                expression_semantics: true,
174                selector_usage: true,
175                style_semantic_graph: true,
176            },
177        ],
178        runner_commands: vec![
179            SelectedQueryRunnerCommandV0 {
180                surface: "sourceResolution",
181                command: "input-source-resolution-canonical-producer",
182                input_contract: "EngineInputV2",
183                output_product: "engine-input-producers.source-resolution-canonical-producer",
184            },
185            SelectedQueryRunnerCommandV0 {
186                surface: "expressionSemantics",
187                command: "input-expression-semantics-canonical-producer",
188                input_contract: "EngineInputV2",
189                output_product: "engine-input-producers.expression-semantics-canonical-producer",
190            },
191            SelectedQueryRunnerCommandV0 {
192                surface: "selectorUsage",
193                command: "input-selector-usage-canonical-producer",
194                input_contract: "EngineInputV2",
195                output_product: "engine-input-producers.selector-usage-canonical-producer",
196            },
197            SelectedQueryRunnerCommandV0 {
198                surface: "styleSemanticGraph",
199                command: "style-semantic-graph",
200                input_contract: "StyleSemanticGraphInputV0",
201                output_product: "omena-semantic.style-semantic-graph",
202            },
203            SelectedQueryRunnerCommandV0 {
204                surface: "styleSemanticGraphBatch",
205                command: "style-semantic-graph-batch",
206                input_contract: "StyleSemanticGraphBatchInputV0",
207                output_product: "omena-semantic.style-semantic-graph-batch",
208            },
209        ],
210        expression_semantics_payload_contracts: vec!["valueDomainKind", "valueDomainDerivation"],
211        required_input_contracts: vec![
212            "EngineInputV2",
213            "StyleSemanticGraphInputV0",
214            "StyleSemanticGraphBatchInputV0",
215        ],
216        adapter_readiness: vec![
217            "backendCapabilityMatrix",
218            "canonicalProducerWrapperBoundary",
219            "styleSemanticGraphBridgeBoundary",
220            "runnerCommandContract",
221            "fragmentBundleBoundary",
222            "expressionSemanticsDerivationPayload",
223        ],
224        routing_status: "declaredOnly",
225    }
226}
227
228pub fn summarize_omena_query_fragment_bundle(input: &EngineInputV2) -> OmenaQueryFragmentBundleV0 {
229    OmenaQueryFragmentBundleV0 {
230        schema_version: "0",
231        product: "omena-query.fragment-bundle",
232        input_version: input.version.clone(),
233        expression_semantics: summarize_omena_query_expression_semantics_query_fragments(input),
234        source_resolution: summarize_omena_query_source_resolution_query_fragments(input),
235        selector_usage: summarize_omena_query_selector_usage_query_fragments(input),
236    }
237}
238
239pub fn summarize_omena_query_expression_semantics_query_fragments(
240    input: &EngineInputV2,
241) -> ExpressionSemanticsQueryFragmentsV0 {
242    summarize_expression_semantics_query_fragments_input(input)
243}
244
245pub fn summarize_omena_query_source_resolution_query_fragments(
246    input: &EngineInputV2,
247) -> SourceResolutionQueryFragmentsV0 {
248    summarize_omena_resolver_query_fragments(input)
249}
250
251pub fn summarize_omena_query_selector_usage_query_fragments(
252    input: &EngineInputV2,
253) -> SelectorUsageQueryFragmentsV0 {
254    summarize_selector_usage_query_fragments_input(input)
255}
256
257pub fn summarize_omena_query_source_resolution_canonical_producer_signal(
258    input: &EngineInputV2,
259) -> SourceResolutionCanonicalProducerSignalV0 {
260    summarize_omena_resolver_canonical_producer_signal(input)
261}
262
263pub fn summarize_omena_query_expression_semantics_canonical_producer_signal(
264    input: &EngineInputV2,
265) -> ExpressionSemanticsCanonicalProducerSignalV0 {
266    summarize_expression_semantics_canonical_producer_signal_input(input)
267}
268
269pub fn summarize_omena_query_selector_usage_canonical_producer_signal(
270    input: &EngineInputV2,
271) -> SelectorUsageCanonicalProducerSignalV0 {
272    summarize_selector_usage_canonical_producer_signal_input(input)
273}
274
275pub fn summarize_omena_query_style_semantic_graph_from_source(
276    style_path: &str,
277    style_source: &str,
278    input: &EngineInputV2,
279) -> Option<StyleSemanticGraphSummaryV0> {
280    summarize_omena_bridge_style_semantic_graph_from_source(style_path, style_source, input)
281}
282
283pub fn summarize_omena_query_style_semantic_graph_batch_from_sources<'a>(
284    styles: impl IntoIterator<Item = (&'a str, &'a str)>,
285    input: &EngineInputV2,
286) -> OmenaQueryStyleSemanticGraphBatchOutputV0 {
287    let graphs = styles
288        .into_iter()
289        .map(
290            |(style_path, style_source)| OmenaQueryStyleSemanticGraphBatchEntryV0 {
291                style_path: style_path.to_string(),
292                graph: summarize_omena_query_style_semantic_graph_from_source(
293                    style_path,
294                    style_source,
295                    input,
296                ),
297            },
298        )
299        .collect::<Vec<_>>();
300
301    OmenaQueryStyleSemanticGraphBatchOutputV0 {
302        schema_version: "0",
303        product: "omena-semantic.style-semantic-graph-batch",
304        graphs,
305    }
306}
307
308#[cfg(test)]
309mod tests {
310    use engine_input_producers::{
311        ClassExpressionInputV2, EngineInputV2, PositionV2, RangeV2, SourceAnalysisInputV2,
312        SourceDocumentV2, StringTypeFactsV2, StyleAnalysisInputV2, StyleDocumentV2,
313        StyleSelectorV2, TypeFactEntryV2,
314    };
315
316    use super::{
317        SelectedQueryAdapterCapabilitiesV0, summarize_omena_query_boundary,
318        summarize_omena_query_expression_semantics_canonical_producer_signal,
319        summarize_omena_query_expression_semantics_query_fragments,
320        summarize_omena_query_fragment_bundle,
321        summarize_omena_query_selected_query_adapter_capabilities,
322        summarize_omena_query_selector_usage_canonical_producer_signal,
323        summarize_omena_query_selector_usage_query_fragments,
324        summarize_omena_query_source_resolution_canonical_producer_signal,
325        summarize_omena_query_source_resolution_query_fragments,
326        summarize_omena_query_style_semantic_graph_batch_from_sources,
327        summarize_omena_query_style_semantic_graph_from_source,
328    };
329
330    #[test]
331    fn summarizes_query_boundary_over_producer_fragments() {
332        let input = sample_input();
333        let summary = summarize_omena_query_boundary(&input);
334
335        assert_eq!(summary.schema_version, "0");
336        assert_eq!(summary.product, "omena-query.boundary");
337        assert_eq!(summary.query_engine_name, "omena-query");
338        assert_eq!(summary.input_version, "2");
339        assert_eq!(
340            summary.abstract_value_domain.product,
341            "omena-abstract-value.domain"
342        );
343        assert_eq!(
344            summary.selected_query_adapter_capabilities.product,
345            "omena-query.selected-query-adapter-capabilities"
346        );
347        assert_eq!(summary.expression_semantics_query_count, 2);
348        assert_eq!(summary.source_resolution_query_count, 2);
349        assert_eq!(summary.selector_usage_query_count, 2);
350        assert_eq!(summary.total_query_count, 6);
351        assert!(
352            summary
353                .ready_surfaces
354                .contains(&"abstractValueProjectionContract")
355        );
356        assert!(
357            summary
358                .ready_surfaces
359                .contains(&"sourceResolutionResolverBoundary")
360        );
361        assert!(
362            summary
363                .delegated_fragment_products
364                .contains(&"omena-resolver.boundary")
365        );
366        assert!(
367            summary
368                .cme_coupled_surfaces
369                .contains(&"producerQueryFragments")
370        );
371    }
372
373    #[test]
374    fn bundles_expression_source_and_selector_query_fragments() {
375        let input = sample_input();
376        let bundle = summarize_omena_query_fragment_bundle(&input);
377
378        assert_eq!(bundle.schema_version, "0");
379        assert_eq!(bundle.product, "omena-query.fragment-bundle");
380        assert_eq!(bundle.input_version, "2");
381        assert_eq!(bundle.expression_semantics.fragments.len(), 2);
382        assert_eq!(bundle.expression_semantics.fragments[0].query_id, "expr-1");
383        assert_eq!(bundle.source_resolution.fragments.len(), 2);
384        assert_eq!(bundle.source_resolution.fragments[1].query_id, "expr-2");
385        assert_eq!(bundle.selector_usage.fragments.len(), 2);
386        assert_eq!(bundle.selector_usage.fragments[0].query_id, "btn-active");
387
388        let expression = summarize_omena_query_expression_semantics_query_fragments(&input);
389        let source = summarize_omena_query_source_resolution_query_fragments(&input);
390        let selector = summarize_omena_query_selector_usage_query_fragments(&input);
391
392        assert_eq!(expression.schema_version, "0");
393        assert_eq!(source.schema_version, "0");
394        assert_eq!(selector.schema_version, "0");
395        assert_eq!(expression.input_version, "2");
396        assert_eq!(source.input_version, "2");
397        assert_eq!(selector.input_version, "2");
398        assert_eq!(
399            expression.fragments.len(),
400            bundle.expression_semantics.fragments.len()
401        );
402        assert_eq!(
403            source.fragments.len(),
404            bundle.source_resolution.fragments.len()
405        );
406        assert_eq!(
407            selector.fragments.len(),
408            bundle.selector_usage.fragments.len()
409        );
410    }
411
412    #[test]
413    fn declares_selected_query_adapter_capabilities_without_flipping_runtime_routing() {
414        let summary = summarize_omena_query_selected_query_adapter_capabilities();
415
416        assert_eq!(summary.schema_version, "0");
417        assert_eq!(
418            summary.product,
419            "omena-query.selected-query-adapter-capabilities"
420        );
421        assert_eq!(summary.default_candidate_backend, "rust-selected-query");
422        assert_eq!(summary.routing_status, "declaredOnly");
423
424        let unified = backend(&summary, "rust-selected-query");
425        assert!(unified.is_some());
426        let Some(unified) = unified else {
427            return;
428        };
429        assert!(unified.source_resolution);
430        assert!(unified.expression_semantics);
431        assert!(unified.selector_usage);
432        assert!(unified.style_semantic_graph);
433
434        let source_only = backend(&summary, "rust-source-resolution");
435        assert!(source_only.is_some());
436        let Some(source_only) = source_only else {
437            return;
438        };
439        assert!(source_only.source_resolution);
440        assert!(!source_only.expression_semantics);
441        assert!(!source_only.selector_usage);
442        assert!(!source_only.style_semantic_graph);
443
444        assert!(
445            summary
446                .runner_commands
447                .iter()
448                .any(|command| command.command == "style-semantic-graph-batch")
449        );
450        assert!(
451            summary
452                .expression_semantics_payload_contracts
453                .contains(&"valueDomainDerivation")
454        );
455        assert!(summary.adapter_readiness.contains(&"runnerCommandContract"));
456        assert!(
457            summary
458                .adapter_readiness
459                .contains(&"canonicalProducerWrapperBoundary")
460        );
461        assert!(
462            summary
463                .adapter_readiness
464                .contains(&"styleSemanticGraphBridgeBoundary")
465        );
466    }
467
468    #[test]
469    fn owns_selected_query_canonical_producer_wrappers_without_changing_products() {
470        let input = sample_input();
471
472        let source = summarize_omena_query_source_resolution_canonical_producer_signal(&input);
473        assert_eq!(source.schema_version, "0");
474        assert_eq!(source.input_version, "2");
475        assert_eq!(source.canonical_bundle.query_fragments.len(), 2);
476        assert_eq!(source.evaluator_candidates.results.len(), 2);
477
478        let expression =
479            summarize_omena_query_expression_semantics_canonical_producer_signal(&input);
480        assert_eq!(expression.schema_version, "0");
481        assert_eq!(expression.input_version, "2");
482        assert_eq!(expression.canonical_bundle.query_fragments.len(), 2);
483        assert_eq!(expression.evaluator_candidates.results.len(), 2);
484        assert_eq!(
485            expression.evaluator_candidates.results[0]
486                .payload
487                .value_domain_derivation
488                .product,
489            "omena-abstract-value.reduced-class-value-derivation"
490        );
491        assert_eq!(
492            expression.evaluator_candidates.results[0]
493                .payload
494                .value_domain_derivation
495                .reduced_kind,
496            "prefixSuffix"
497        );
498
499        let selector = summarize_omena_query_selector_usage_canonical_producer_signal(&input);
500        assert_eq!(selector.schema_version, "0");
501        assert_eq!(selector.input_version, "2");
502        assert_eq!(selector.canonical_bundle.query_fragments.len(), 2);
503        assert_eq!(selector.evaluator_candidates.results.len(), 2);
504    }
505
506    #[test]
507    fn owns_style_semantic_graph_adapter_boundary_without_changing_graph_product() {
508        let input = sample_input();
509        let graph = summarize_omena_query_style_semantic_graph_from_source(
510            "/tmp/App.module.scss",
511            ".btn-active { color: red; }",
512            &input,
513        );
514        assert!(graph.is_some());
515        let Some(graph) = graph else {
516            return;
517        };
518        assert_eq!(graph.schema_version, "0");
519        assert_eq!(graph.product, "omena-semantic.style-semantic-graph");
520        assert_eq!(graph.selector_identity_engine.canonical_ids.len(), 1);
521
522        let batch = summarize_omena_query_style_semantic_graph_batch_from_sources(
523            [
524                ("/tmp/App.module.scss", ".btn-active { color: red; }"),
525                ("/tmp/Card.module.scss", ".card-header { color: blue; }"),
526            ],
527            &input,
528        );
529        assert_eq!(batch.schema_version, "0");
530        assert_eq!(batch.product, "omena-semantic.style-semantic-graph-batch");
531        assert_eq!(batch.graphs.len(), 2);
532        assert_eq!(batch.graphs[0].style_path, "/tmp/App.module.scss");
533        assert!(batch.graphs[0].graph.is_some());
534        assert!(batch.graphs[1].graph.is_some());
535    }
536
537    fn backend<'a>(
538        summary: &'a SelectedQueryAdapterCapabilitiesV0,
539        backend_kind: &str,
540    ) -> Option<&'a super::SelectedQueryBackendCapabilityV0> {
541        summary
542            .backend_kinds
543            .iter()
544            .find(|backend| backend.backend_kind == backend_kind)
545    }
546
547    fn sample_input() -> EngineInputV2 {
548        EngineInputV2 {
549            version: "2".to_string(),
550            sources: vec![SourceAnalysisInputV2 {
551                document: SourceDocumentV2 {
552                    class_expressions: vec![
553                        ClassExpressionInputV2 {
554                            id: "expr-1".to_string(),
555                            kind: "symbolRef".to_string(),
556                            scss_module_path: "/tmp/App.module.scss".to_string(),
557                            range: range(4, 12, 4, 16),
558                            class_name: None,
559                            root_binding_decl_id: Some("decl-1".to_string()),
560                            access_path: None,
561                        },
562                        ClassExpressionInputV2 {
563                            id: "expr-2".to_string(),
564                            kind: "styleAccess".to_string(),
565                            scss_module_path: "/tmp/Card.module.scss".to_string(),
566                            range: range(6, 9, 6, 20),
567                            class_name: Some("card-header".to_string()),
568                            root_binding_decl_id: None,
569                            access_path: Some(vec!["card".to_string(), "header".to_string()]),
570                        },
571                    ],
572                },
573            }],
574            styles: vec![
575                StyleAnalysisInputV2 {
576                    file_path: "/tmp/App.module.scss".to_string(),
577                    document: StyleDocumentV2 {
578                        selectors: vec![StyleSelectorV2 {
579                            name: "btn-active".to_string(),
580                            view_kind: "canonical".to_string(),
581                            canonical_name: Some("btn-active".to_string()),
582                            range: range(1, 1, 1, 12),
583                            nested_safety: Some("safe".to_string()),
584                            composes: None,
585                            bem_suffix: None,
586                        }],
587                    },
588                },
589                StyleAnalysisInputV2 {
590                    file_path: "/tmp/Card.module.scss".to_string(),
591                    document: StyleDocumentV2 {
592                        selectors: vec![StyleSelectorV2 {
593                            name: "card-header".to_string(),
594                            view_kind: "canonical".to_string(),
595                            canonical_name: Some("card-header".to_string()),
596                            range: range(3, 1, 3, 13),
597                            nested_safety: Some("unsafe".to_string()),
598                            composes: None,
599                            bem_suffix: None,
600                        }],
601                    },
602                },
603            ],
604            type_facts: vec![
605                TypeFactEntryV2 {
606                    file_path: "/tmp/App.tsx".to_string(),
607                    expression_id: "expr-1".to_string(),
608                    facts: StringTypeFactsV2 {
609                        kind: "constrained".to_string(),
610                        constraint_kind: Some("prefixSuffix".to_string()),
611                        values: None,
612                        prefix: Some("btn-".to_string()),
613                        suffix: Some("-active".to_string()),
614                        min_len: Some(10),
615                        max_len: None,
616                        char_must: None,
617                        char_may: None,
618                        may_include_other_chars: None,
619                    },
620                },
621                TypeFactEntryV2 {
622                    file_path: "/tmp/Card.tsx".to_string(),
623                    expression_id: "expr-2".to_string(),
624                    facts: StringTypeFactsV2 {
625                        kind: "finiteSet".to_string(),
626                        constraint_kind: None,
627                        values: Some(vec!["card-header".to_string(), "card-body".to_string()]),
628                        prefix: None,
629                        suffix: None,
630                        min_len: None,
631                        max_len: None,
632                        char_must: None,
633                        char_may: None,
634                        may_include_other_chars: None,
635                    },
636                },
637            ],
638        }
639    }
640
641    fn range(
642        start_line: usize,
643        start_character: usize,
644        end_line: usize,
645        end_character: usize,
646    ) -> RangeV2 {
647        RangeV2 {
648            start: PositionV2 {
649                line: start_line,
650                character: start_character,
651            },
652            end: PositionV2 {
653                line: end_line,
654                character: end_character,
655            },
656        }
657    }
658}