Skip to main content

omena_query_core/
lib.rs

1//! Core query runtime primitives below the public `omena-query` facade.
2//!
3//! This crate owns producer-fragment summaries and expression-domain runtime
4//! state. `omena-query` re-exports these surfaces, but no longer needs to depend
5//! directly on each lower-level producer crate for this part of the dataflow.
6
7#[cfg(feature = "test-support")]
8use std::cell::Cell;
9use std::collections::{BTreeMap, BTreeSet};
10
11pub use engine_input_producers::{
12    ClassExpressionInputV2, EngineInputV2, ExpressionDomainCallSiteFlowAnalysisV0,
13    ExpressionDomainControlFlowAnalysisV0, ExpressionDomainFlowAnalysisV0,
14    ExpressionDomainProvenanceExplanationsV0, ExpressionDomainReducedProductIterationV0,
15    ExpressionSemanticsCanonicalProducerSignalV0, ExpressionSemanticsQueryFragmentsV0, PositionV2,
16    RangeV2, SelectorUsageCanonicalProducerSignalV0, SelectorUsageQueryFragmentsV0,
17    SourceAnalysisInputV2, SourceDocumentV2, SourceResolutionCanonicalProducerSignalV0,
18    SourceResolutionQueryFragmentsV0, StringTypeFactsV2, StyleAnalysisInputV2, StyleDocumentV2,
19    StyleSelectorV2, TypeFactEntryV2,
20};
21use engine_input_producers::{
22    collect_expression_domain_flow_graphs,
23    summarize_expression_domain_call_site_flow_analysis_input,
24    summarize_expression_domain_control_flow_analysis_input,
25    summarize_expression_domain_flow_analysis_input,
26    summarize_expression_domain_provenance_explanations_input,
27    summarize_expression_domain_reduced_product_iteration_input,
28    summarize_expression_semantics_canonical_producer_signal_input,
29    summarize_expression_semantics_query_fragments_input,
30    summarize_selector_usage_canonical_producer_signal_input,
31    summarize_selector_usage_query_fragments_input,
32};
33use omena_abstract_value::{
34    AbstractClassValueProvenanceV0, analyze_class_value_flow_incremental_with_database,
35    project_abstract_value_selectors, summarize_omena_abstract_value_domain,
36    summarize_reduced_class_value_product,
37};
38pub use omena_abstract_value::{
39    AbstractClassValueV0, AbstractPropertyValueCandidateV0, AbstractPropertyValueNarrowingV0,
40    AbstractPropertyValueV0, AbstractValueDomainSummaryV0, CascadeContextV0,
41    CascadeValueFamilyMemberV0, ClassBoundaryEffectV0, ClassValueFlowAnalysisV0,
42    ClassValueFlowIncrementalAnalysisV0, CssValueValidationClassV0, ExternalStringTypeFactsV0,
43    FactPrecision, FirstWitnessErrorV0, GuardAtomV0, GuardedTokenInputV0, GuardedTokenLanguageV0,
44    GuardedTokenMapInputV0, GuardedTokenMapV0, GuardedTokenObserverV0, Lin01ProvenanceSemiringV0,
45    LinearProvenancePathV0, LinearProvenanceV0, NaturalCountProvenanceSemiringV0,
46    OmenaAbstractValueCoverageDirectionV0, OmenaAbstractValuePrecisionBasisV0,
47    OmenaAbstractValuePrecisionWitnessV0, PolynomialProvenanceProjectionV0,
48    PolynomialProvenanceTermV0, PolynomialProvenanceV0, PolynomialProvenanceVariableV0,
49    ProvenanceSemiringLawReportV0, ReducedClassValueProductIterationV0, ReducedClassValueProductV0,
50    SelectorProjectionCertaintyV0, TokenObserverProjectionV0, abstract_class_value_from_facts,
51    abstract_class_value_kind, derive_context_indexed_cascade_restriction_maps_v0,
52    fact_precision_from_class_value, fact_precision_from_class_value_with_witness,
53    iterate_reduced_class_value_product_constraints, join_abstract_class_values,
54    narrow_abstract_property_value_for_cascade_branch,
55    narrow_abstract_property_value_for_pseudo_state, prefix_suffix_class_value,
56    summarize_context_indexed_cascade_value_family_v0,
57    summarize_polynomial_provenance_from_linear_v0, top_class_value,
58    validate_registered_property_value_v0, verify_provenance_semiring_laws_on_fixtures,
59};
60#[allow(deprecated)]
61#[deprecated(
62    since = "0.4.0",
63    note = "use the context-indexed cascade value family adapters; removal is not before 1.0 and requires downstream migration plus zero audited non-compatibility uses"
64)]
65pub use omena_abstract_value::{
66    derive_cascade_restriction_maps_v0, summarize_cascade_value_family_v0,
67};
68pub use omena_incremental::{
69    IncrementalEditDistancePriorityInputV0, IncrementalGraphInputV0,
70    IncrementalInvalidationPriorityPlanV0, IncrementalNodeInputV0, IncrementalRevisionV0,
71    OmenaIncrementalDatabaseV0, OmenaSalsaDatabaseV0, OmenaWorkspaceSnapshotIdV0,
72    snapshot_from_graph_input,
73};
74pub use omena_refinement::{
75    CascadeDimensionalRefinementBridgeV0, RefinementPropertyPredicateV0,
76    summarize_cascade_dimensional_refinement_bridge_v0,
77};
78pub use omena_resolver::OmenaResolverSourceResolutionRuntimeIndexV0;
79use omena_resolver::{
80    summarize_omena_resolver_canonical_producer_signal, summarize_omena_resolver_query_fragments,
81    summarize_omena_resolver_source_resolution_runtime,
82};
83pub use omena_value_lattice::{
84    canonicalize_css_value, split_top_level_value_arguments,
85    split_top_level_whitespace_value_components,
86};
87use serde::{Deserialize, Serialize};
88
89pub const OMENA_QUERY_CURRENT_SCHEMA_VERSION: &str = "0";
90pub const OMENA_QUERY_CURRENT_SCHEMA_VERSION_LABEL: &str = "V0";
91
92#[cfg(feature = "test-support")]
93thread_local! {
94    static SELECTOR_PROJECTION_EVALUATION_COUNT: Cell<usize> = const { Cell::new(0) };
95}
96
97#[cfg(feature = "test-support")]
98pub fn reset_selector_projection_evaluation_count_for_test() {
99    SELECTOR_PROJECTION_EVALUATION_COUNT.with(|counter| counter.set(0));
100}
101
102#[cfg(feature = "test-support")]
103pub fn selector_projection_evaluation_count_for_test() -> usize {
104    SELECTOR_PROJECTION_EVALUATION_COUNT.with(Cell::get)
105}
106
107#[cfg(feature = "test-support")]
108fn record_selector_projection_evaluation_for_test() {
109    SELECTOR_PROJECTION_EVALUATION_COUNT.with(|counter| counter.set(counter.get() + 1));
110}
111
112#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
113#[serde(rename_all = "camelCase")]
114pub struct OmenaQueryAnalysisPrecisionV0 {
115    pub product: String,
116    pub value_domain: String,
117    pub flow_sensitivity: String,
118    pub context_sensitivity: String,
119    pub revision_axis: String,
120}
121
122const OMENA_QUERY_ANALYSIS_FACT_PRECISION_BY_VALUE_DOMAIN: &[(&str, FactPrecision)] = &[
123    ("cascadeAtPosition", FactPrecision::Exact),
124    ("styleModuleResolution", FactPrecision::Exact),
125    ("classValueResolution", FactPrecision::Conservative),
126    ("classValueUniverse", FactPrecision::Conservative),
127    ("classValueFlow", FactPrecision::Heuristic),
128    ("unknown", FactPrecision::Unknown),
129];
130
131pub fn fact_precision_from_analysis_precision(
132    precision: &OmenaQueryAnalysisPrecisionV0,
133) -> FactPrecision {
134    OMENA_QUERY_ANALYSIS_FACT_PRECISION_BY_VALUE_DOMAIN
135        .iter()
136        .find_map(|(value_domain, mapped)| {
137            (*value_domain == precision.value_domain).then_some(*mapped)
138        })
139        .unwrap_or(FactPrecision::Unknown)
140}
141
142#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
143#[serde(rename_all = "camelCase")]
144pub struct OmenaQueryAnalysisResultV0<TValue> {
145    pub schema_version: String,
146    pub product: String,
147    pub value: TValue,
148    pub precision: OmenaQueryAnalysisPrecisionV0,
149    pub provenance: Vec<String>,
150    pub revision: u64,
151}
152
153impl<TValue> OmenaQueryAnalysisResultV0<TValue> {
154    pub fn new(
155        value: TValue,
156        precision: OmenaQueryAnalysisPrecisionV0,
157        provenance: Vec<String>,
158        revision: u64,
159    ) -> Self {
160        Self {
161            schema_version: OMENA_QUERY_CURRENT_SCHEMA_VERSION.to_string(),
162            product: "omena-query.analysis-result".to_string(),
163            value,
164            precision,
165            provenance,
166            revision,
167        }
168    }
169}
170
171#[derive(Debug, Serialize)]
172#[serde(rename_all = "camelCase")]
173pub struct OmenaQueryFragmentBundleV0 {
174    pub schema_version: &'static str,
175    pub product: &'static str,
176    pub input_version: String,
177    pub expression_semantics: ExpressionSemanticsQueryFragmentsV0,
178    pub source_resolution: SourceResolutionQueryFragmentsV0,
179    pub selector_usage: SelectorUsageQueryFragmentsV0,
180}
181
182#[derive(Debug, Serialize)]
183#[serde(rename_all = "camelCase")]
184pub struct OmenaQueryExpressionDomainIncrementalFlowAnalysisV0 {
185    pub schema_version: &'static str,
186    pub product: &'static str,
187    pub input_version: String,
188    pub revision: u64,
189    pub graph_count: usize,
190    pub dirty_graph_count: usize,
191    pub reused_graph_count: usize,
192    pub analyses: Vec<OmenaQueryExpressionDomainIncrementalFlowAnalysisEntryV0>,
193}
194
195#[derive(Debug, Serialize)]
196#[serde(rename_all = "camelCase")]
197pub struct OmenaQueryExpressionDomainIncrementalFlowAnalysisEntryV0 {
198    pub graph_id: String,
199    pub file_path: String,
200    pub analysis: ClassValueFlowIncrementalAnalysisV0,
201}
202
203#[derive(Debug, Serialize)]
204#[serde(rename_all = "camelCase")]
205pub struct OmenaQueryExpressionDomainSelectorProjectionV0 {
206    pub schema_version: &'static str,
207    pub product: &'static str,
208    pub input_version: String,
209    pub projection_count: usize,
210    pub projections: Vec<OmenaQueryExpressionDomainSelectorProjectionEntryV0>,
211}
212
213#[derive(Debug, Serialize)]
214#[serde(rename_all = "camelCase")]
215pub struct OmenaQueryExpressionDomainSelectorProjectionEntryV0 {
216    pub graph_id: String,
217    pub file_path: String,
218    pub node_id: String,
219    pub target_style_paths: Vec<String>,
220    pub value_kind: &'static str,
221    #[serde(skip_serializing_if = "Option::is_none")]
222    pub reduced_product: Option<ReducedClassValueProductV0>,
223    pub selector_names: Vec<String>,
224    pub certainty: SelectorProjectionCertaintyV0,
225}
226
227#[derive(Debug, Clone, PartialEq, Eq, Serialize)]
228#[serde(rename_all = "camelCase")]
229pub struct OmenaQueryExpressionDomainSelectorPrecisionV0 {
230    pub graph_id: String,
231    pub node_id: String,
232    pub precision: FactPrecision,
233}
234
235#[derive(Debug, Clone, PartialEq, Eq)]
236struct ExpressionDomainSelectorCertaintyFlowHedgeV0 {
237    graph_converged: bool,
238    contains_flow_iteration_limit: bool,
239}
240
241#[derive(Default)]
242pub struct OmenaQueryExpressionDomainFlowRuntimeV0 {
243    revision: u64,
244    databases_by_graph_id: BTreeMap<String, OmenaIncrementalDatabaseV0>,
245    previous_analyses_by_graph_id: BTreeMap<String, ClassValueFlowAnalysisV0>,
246}
247
248impl OmenaQueryExpressionDomainFlowRuntimeV0 {
249    pub fn revision(&self) -> u64 {
250        self.revision
251    }
252
253    pub fn graph_count(&self) -> usize {
254        self.databases_by_graph_id.len()
255    }
256
257    pub fn analyze_input(
258        &mut self,
259        input: &EngineInputV2,
260    ) -> OmenaQueryExpressionDomainIncrementalFlowAnalysisV0 {
261        self.revision += 1;
262        let revision = self.revision;
263        let flow_graphs = collect_expression_domain_flow_graphs(input);
264        let live_graph_ids = flow_graphs
265            .iter()
266            .map(|entry| entry.graph_id.clone())
267            .collect::<BTreeSet<_>>();
268
269        self.databases_by_graph_id
270            .retain(|graph_id, _| live_graph_ids.contains(graph_id));
271        self.previous_analyses_by_graph_id
272            .retain(|graph_id, _| live_graph_ids.contains(graph_id));
273
274        let analyses = flow_graphs
275            .into_iter()
276            .map(|entry| {
277                let database = self
278                    .databases_by_graph_id
279                    .entry(entry.graph_id.clone())
280                    .or_default();
281                let previous_analysis = self.previous_analyses_by_graph_id.get(&entry.graph_id);
282                let analysis = analyze_class_value_flow_incremental_with_database(
283                    &entry.graph,
284                    database,
285                    previous_analysis,
286                    revision,
287                );
288                self.previous_analyses_by_graph_id
289                    .insert(entry.graph_id.clone(), analysis.analysis.clone());
290
291                OmenaQueryExpressionDomainIncrementalFlowAnalysisEntryV0 {
292                    graph_id: entry.graph_id,
293                    file_path: entry.file_path,
294                    analysis,
295                }
296            })
297            .collect::<Vec<_>>();
298
299        let dirty_graph_count = analyses
300            .iter()
301            .filter(|entry| entry.analysis.incremental_plan.dirty_node_count > 0)
302            .count();
303        let reused_graph_count = analyses
304            .iter()
305            .filter(|entry| entry.analysis.reused_previous_analysis)
306            .count();
307
308        OmenaQueryExpressionDomainIncrementalFlowAnalysisV0 {
309            schema_version: OMENA_QUERY_CURRENT_SCHEMA_VERSION,
310            product: "omena-query.expression-domain-incremental-flow-analysis",
311            input_version: input.version.clone(),
312            revision,
313            graph_count: analyses.len(),
314            dirty_graph_count,
315            reused_graph_count,
316            analyses,
317        }
318    }
319}
320
321pub fn summarize_omena_query_core_abstract_value_domain() -> AbstractValueDomainSummaryV0 {
322    summarize_omena_abstract_value_domain()
323}
324
325pub fn summarize_omena_query_fragment_bundle(input: &EngineInputV2) -> OmenaQueryFragmentBundleV0 {
326    OmenaQueryFragmentBundleV0 {
327        schema_version: OMENA_QUERY_CURRENT_SCHEMA_VERSION,
328        product: "omena-query.fragment-bundle",
329        input_version: input.version.clone(),
330        expression_semantics: summarize_omena_query_expression_semantics_query_fragments(input),
331        source_resolution: summarize_omena_query_source_resolution_query_fragments(input),
332        selector_usage: summarize_omena_query_selector_usage_query_fragments(input),
333    }
334}
335
336pub fn summarize_omena_query_expression_semantics_query_fragments(
337    input: &EngineInputV2,
338) -> ExpressionSemanticsQueryFragmentsV0 {
339    summarize_expression_semantics_query_fragments_input(input)
340}
341
342pub fn summarize_omena_query_expression_domain_flow_analysis(
343    input: &EngineInputV2,
344) -> ExpressionDomainFlowAnalysisV0 {
345    summarize_expression_domain_flow_analysis_input(input)
346}
347
348pub fn summarize_omena_query_expression_domain_control_flow_analysis(
349    input: &EngineInputV2,
350) -> ExpressionDomainControlFlowAnalysisV0 {
351    summarize_expression_domain_control_flow_analysis_input(input)
352}
353
354pub fn summarize_omena_query_expression_domain_call_site_flow_analysis(
355    input: &EngineInputV2,
356) -> ExpressionDomainCallSiteFlowAnalysisV0 {
357    summarize_expression_domain_call_site_flow_analysis_input(input)
358}
359
360pub fn summarize_omena_query_expression_domain_provenance_explanations(
361    input: &EngineInputV2,
362) -> ExpressionDomainProvenanceExplanationsV0 {
363    summarize_expression_domain_provenance_explanations_input(input)
364}
365
366pub fn summarize_omena_query_expression_domain_reduced_product_iteration(
367    input: &EngineInputV2,
368) -> ExpressionDomainReducedProductIterationV0 {
369    summarize_expression_domain_reduced_product_iteration_input(input)
370}
371
372pub fn summarize_omena_query_expression_domain_incremental_flow_analysis(
373    input: &EngineInputV2,
374    runtime: &mut OmenaQueryExpressionDomainFlowRuntimeV0,
375) -> OmenaQueryExpressionDomainIncrementalFlowAnalysisV0 {
376    runtime.analyze_input(input)
377}
378
379pub fn summarize_omena_query_expression_domain_incremental_flow_analysis_result(
380    input: &EngineInputV2,
381    runtime: &mut OmenaQueryExpressionDomainFlowRuntimeV0,
382) -> OmenaQueryAnalysisResultV0<OmenaQueryExpressionDomainIncrementalFlowAnalysisV0> {
383    let value = runtime.analyze_input(input);
384    let revision = value.revision;
385    OmenaQueryAnalysisResultV0::new(
386        value,
387        OmenaQueryAnalysisPrecisionV0 {
388            product: "omena-query.analysis-precision".to_string(),
389            value_domain: "classValueFlow".to_string(),
390            flow_sensitivity: "incrementalDataflow".to_string(),
391            context_sensitivity: "perExpressionGraph".to_string(),
392            revision_axis: "OmenaQueryExpressionDomainFlowRuntimeV0.revision".to_string(),
393        },
394        vec![
395            "omena-query-core.expression-domain-runtime".to_string(),
396            "omena-abstract-value.incremental-class-value-flow".to_string(),
397        ],
398        revision,
399    )
400}
401
402pub fn summarize_omena_query_expression_domain_selector_projection(
403    input: &EngineInputV2,
404) -> OmenaQueryExpressionDomainSelectorProjectionV0 {
405    summarize_omena_query_expression_domain_selector_projection_with_precision(input).0
406}
407
408pub fn summarize_omena_query_expression_domain_selector_projection_with_precision(
409    input: &EngineInputV2,
410) -> (
411    OmenaQueryExpressionDomainSelectorProjectionV0,
412    Vec<OmenaQueryExpressionDomainSelectorPrecisionV0>,
413) {
414    summarize_omena_query_expression_domain_selector_projection_with_precision_and_style_path_resolver(
415        input,
416        |target, _known| Some(target.to_string()),
417    )
418}
419
420pub fn summarize_omena_query_expression_domain_selector_projection_with_precision_and_style_path_resolver<
421    F,
422>(
423    input: &EngineInputV2,
424    resolve_style_path: F,
425) -> (
426    OmenaQueryExpressionDomainSelectorProjectionV0,
427    Vec<OmenaQueryExpressionDomainSelectorPrecisionV0>,
428)
429where
430    F: Fn(&str, &[String]) -> Option<String>,
431{
432    #[cfg(feature = "test-support")]
433    record_selector_projection_evaluation_for_test();
434    let selector_certainty_flow_hedges = expression_domain_selector_certainty_flow_hedges(input);
435    summarize_omena_query_expression_domain_selector_projection_with_flow_hedges(
436        input,
437        resolve_style_path,
438        &selector_certainty_flow_hedges,
439    )
440}
441
442fn summarize_omena_query_expression_domain_selector_projection_with_flow_hedges<F>(
443    input: &EngineInputV2,
444    resolve_style_path: F,
445    selector_certainty_flow_hedges: &BTreeMap<
446        (String, String),
447        ExpressionDomainSelectorCertaintyFlowHedgeV0,
448    >,
449) -> (
450    OmenaQueryExpressionDomainSelectorProjectionV0,
451    Vec<OmenaQueryExpressionDomainSelectorPrecisionV0>,
452)
453where
454    F: Fn(&str, &[String]) -> Option<String>,
455{
456    let style_selectors_by_path = style_selector_universe_by_path(input);
457    let known_style_paths = style_selectors_by_path.keys().cloned().collect::<Vec<_>>();
458    let expression_targets = expression_target_style_paths(input);
459    let flow_analysis = summarize_omena_query_expression_domain_flow_analysis(input);
460    let mut projections = Vec::new();
461    let mut precisions = Vec::new();
462
463    for graph in flow_analysis.analyses {
464        for node in graph.analysis.nodes {
465            let target_style_paths = target_style_paths_for_flow_node(
466                node.id.as_str(),
467                node.predecessor_ids.as_slice(),
468                &expression_targets,
469            );
470            let resolved_target_style_paths = target_style_paths
471                .iter()
472                .map(|path| resolve_style_path(path, known_style_paths.as_slice()))
473                .collect::<Option<Vec<_>>>();
474            let selector_universe = selector_universe_for_targets(
475                resolved_target_style_paths.as_deref(),
476                &style_selectors_by_path,
477            );
478            let projection = project_abstract_value_selectors(&node.value, &selector_universe);
479            let certainty = hedge_selector_projection_certainty(
480                projection.certainty,
481                selector_certainty_flow_hedges.get(&(graph.file_path.clone(), node.id.clone())),
482            );
483            precisions.push(OmenaQueryExpressionDomainSelectorPrecisionV0 {
484                graph_id: graph.graph_id.clone(),
485                node_id: node.id.clone(),
486                precision: fact_precision_from_class_value(&node.value),
487            });
488            projections.push(OmenaQueryExpressionDomainSelectorProjectionEntryV0 {
489                graph_id: graph.graph_id.clone(),
490                file_path: graph.file_path.clone(),
491                node_id: node.id,
492                target_style_paths,
493                value_kind: node.value_kind,
494                reduced_product: summarize_reduced_class_value_product(&node.value),
495                selector_names: projection.selector_names,
496                certainty,
497            });
498        }
499    }
500
501    (
502        OmenaQueryExpressionDomainSelectorProjectionV0 {
503            schema_version: OMENA_QUERY_CURRENT_SCHEMA_VERSION,
504            product: "omena-query.expression-domain-selector-projection",
505            input_version: input.version.clone(),
506            projection_count: projections.len(),
507            projections,
508        },
509        precisions,
510    )
511}
512
513fn expression_domain_selector_certainty_flow_hedges(
514    input: &EngineInputV2,
515) -> BTreeMap<(String, String), ExpressionDomainSelectorCertaintyFlowHedgeV0> {
516    bind_expression_domain_selector_certainty_flow_hedges(
517        input,
518        summarize_omena_query_expression_domain_control_flow_analysis(input),
519    )
520}
521
522fn bind_expression_domain_selector_certainty_flow_hedges(
523    input: &EngineInputV2,
524    control_flow: ExpressionDomainControlFlowAnalysisV0,
525) -> BTreeMap<(String, String), ExpressionDomainSelectorCertaintyFlowHedgeV0> {
526    let control_flow_facts = input
527        .type_facts
528        .iter()
529        .filter(|entry| entry.control_flow_graph.is_some())
530        .collect::<Vec<_>>();
531    assert_eq!(
532        control_flow_facts.len(),
533        control_flow.analyses.len(),
534        "selector-certainty flow hedge requires one control analysis per control-flow type fact"
535    );
536
537    let mut hedges = BTreeMap::new();
538    for (entry, analyzed) in control_flow_facts.into_iter().zip(control_flow.analyses) {
539        let diagnostic_graph_id = format!(
540            "{}:{}:expression-domain-control-flow",
541            entry.file_path, entry.expression_id
542        );
543        assert_eq!(
544            analyzed.file_path, entry.file_path,
545            "selector-certainty flow hedge control analysis order/file mismatch"
546        );
547        assert_eq!(
548            analyzed.graph_id, diagnostic_graph_id,
549            "selector-certainty flow hedge control analysis order/graph mismatch"
550        );
551
552        let contains_flow_iteration_limit = analyzed
553            .analysis
554            .flow_analysis
555            .nodes
556            .iter()
557            .any(|node| abstract_value_contains_flow_iteration_limit(&node.value));
558        let key = (entry.file_path.clone(), entry.expression_id.clone());
559        let previous = hedges.insert(
560            key.clone(),
561            ExpressionDomainSelectorCertaintyFlowHedgeV0 {
562                graph_converged: analyzed.analysis.flow_analysis.converged,
563                contains_flow_iteration_limit,
564            },
565        );
566        assert!(
567            previous.is_none(),
568            "selector-certainty flow hedge duplicate type-fact key: file_path={:?} expression_id={:?}",
569            key.0,
570            key.1
571        );
572    }
573
574    hedges
575}
576
577fn hedge_selector_projection_certainty(
578    base: SelectorProjectionCertaintyV0,
579    flow_hedge: Option<&ExpressionDomainSelectorCertaintyFlowHedgeV0>,
580) -> SelectorProjectionCertaintyV0 {
581    flow_hedge.map_or(base, |hedge| {
582        hedge_selector_certainty_for_flow(
583            base,
584            hedge.graph_converged,
585            hedge.contains_flow_iteration_limit,
586        )
587    })
588}
589
590fn hedge_selector_certainty_for_flow(
591    base: SelectorProjectionCertaintyV0,
592    graph_converged: bool,
593    contains_flow_iteration_limit: bool,
594) -> SelectorProjectionCertaintyV0 {
595    if graph_converged && !contains_flow_iteration_limit {
596        base
597    } else {
598        SelectorProjectionCertaintyV0::Possible
599    }
600}
601
602fn abstract_value_contains_flow_iteration_limit(value: &AbstractClassValueV0) -> bool {
603    let provenance = match value {
604        AbstractClassValueV0::Automaton { provenance, .. }
605        | AbstractClassValueV0::Prefix { provenance, .. }
606        | AbstractClassValueV0::Suffix { provenance, .. }
607        | AbstractClassValueV0::PrefixSuffix { provenance, .. }
608        | AbstractClassValueV0::CharInclusion { provenance, .. }
609        | AbstractClassValueV0::Composite { provenance, .. }
610        | AbstractClassValueV0::Top { provenance } => *provenance,
611        AbstractClassValueV0::Bottom
612        | AbstractClassValueV0::Exact { .. }
613        | AbstractClassValueV0::FiniteSet { .. } => None,
614    };
615
616    provenance == Some(AbstractClassValueProvenanceV0::FlowIterationLimit)
617}
618
619fn expression_target_style_paths(input: &EngineInputV2) -> BTreeMap<String, String> {
620    input
621        .sources
622        .iter()
623        .flat_map(|source| source.document.class_expressions.iter())
624        .map(|expression| (expression.id.clone(), expression.scss_module_path.clone()))
625        .collect()
626}
627
628fn style_selector_universe_by_path(input: &EngineInputV2) -> BTreeMap<String, Vec<String>> {
629    input
630        .styles
631        .iter()
632        .map(|style| {
633            let selector_names = style
634                .document
635                .selectors
636                .iter()
637                .map(|selector| {
638                    selector
639                        .canonical_name
640                        .clone()
641                        .unwrap_or_else(|| selector.name.clone())
642                })
643                .collect::<BTreeSet<_>>()
644                .into_iter()
645                .collect::<Vec<_>>();
646            (style.file_path.clone(), selector_names)
647        })
648        .collect()
649}
650
651fn target_style_paths_for_flow_node(
652    node_id: &str,
653    predecessor_ids: &[String],
654    expression_targets: &BTreeMap<String, String>,
655) -> Vec<String> {
656    let mut targets = BTreeSet::new();
657    if let Some(target) = expression_targets.get(node_id) {
658        targets.insert(target.clone());
659    }
660    for predecessor_id in predecessor_ids {
661        if let Some(target) = expression_targets.get(predecessor_id) {
662            targets.insert(target.clone());
663        }
664    }
665    targets.into_iter().collect()
666}
667
668fn selector_universe_for_targets(
669    target_style_paths: Option<&[String]>,
670    style_selectors_by_path: &BTreeMap<String, Vec<String>>,
671) -> Vec<String> {
672    let mut selectors = BTreeSet::new();
673    if target_style_paths.is_none_or(<[String]>::is_empty) {
674        for selector_names in style_selectors_by_path.values() {
675            selectors.extend(selector_names.iter().cloned());
676        }
677    } else if let Some(target_style_paths) = target_style_paths {
678        for target_style_path in target_style_paths {
679            if let Some(selector_names) = style_selectors_by_path.get(target_style_path) {
680                selectors.extend(selector_names.iter().cloned());
681            }
682        }
683    }
684    selectors.into_iter().collect()
685}
686
687pub fn summarize_omena_query_source_resolution_query_fragments(
688    input: &EngineInputV2,
689) -> SourceResolutionQueryFragmentsV0 {
690    summarize_omena_resolver_query_fragments(input)
691}
692
693pub fn summarize_omena_query_selector_usage_query_fragments(
694    input: &EngineInputV2,
695) -> SelectorUsageQueryFragmentsV0 {
696    summarize_selector_usage_query_fragments_input(input)
697}
698
699pub fn summarize_omena_query_source_resolution_canonical_producer_signal(
700    input: &EngineInputV2,
701) -> SourceResolutionCanonicalProducerSignalV0 {
702    summarize_omena_resolver_canonical_producer_signal(input)
703}
704
705pub fn summarize_omena_query_source_resolution_runtime(
706    input: &EngineInputV2,
707) -> OmenaResolverSourceResolutionRuntimeIndexV0 {
708    summarize_omena_resolver_source_resolution_runtime(input)
709}
710
711pub fn summarize_omena_query_expression_semantics_canonical_producer_signal(
712    input: &EngineInputV2,
713) -> ExpressionSemanticsCanonicalProducerSignalV0 {
714    summarize_expression_semantics_canonical_producer_signal_input(input)
715}
716
717pub fn summarize_omena_query_selector_usage_canonical_producer_signal(
718    input: &EngineInputV2,
719) -> SelectorUsageCanonicalProducerSignalV0 {
720    summarize_selector_usage_canonical_producer_signal_input(input)
721}
722
723#[cfg(test)]
724mod tests {
725    use super::*;
726
727    fn selector_certainty_product_input() -> EngineInputV2 {
728        let range = RangeV2 {
729            start: PositionV2 {
730                line: 0,
731                character: 0,
732            },
733            end: PositionV2 {
734                line: 0,
735                character: 11,
736            },
737        };
738        EngineInputV2 {
739            version: "selector-certainty-product".to_string(),
740            sources: vec![SourceAnalysisInputV2 {
741                document: SourceDocumentV2 {
742                    class_expressions: vec![ClassExpressionInputV2 {
743                        id: "expr-certainty".to_string(),
744                        kind: "styleAccess".to_string(),
745                        scss_module_path: "/tmp/App.module.scss".to_string(),
746                        range: range.clone(),
747                        class_name: Some("x".to_string()),
748                        root_binding_decl_id: None,
749                        access_path: Some(vec!["styles".to_string(), "x".to_string()]),
750                    }],
751                },
752            }],
753            styles: vec![StyleAnalysisInputV2 {
754                file_path: "/tmp/App.module.scss".to_string(),
755                source: None,
756                document: StyleDocumentV2 {
757                    selectors: vec![StyleSelectorV2 {
758                        name: "x".to_string(),
759                        view_kind: "canonical".to_string(),
760                        canonical_name: Some("x".to_string()),
761                        range,
762                        nested_safety: Some("safe".to_string()),
763                        composes: None,
764                        bem_suffix: None,
765                    }],
766                },
767            }],
768            type_facts: vec![TypeFactEntryV2 {
769                file_path: "/tmp/App.tsx".to_string(),
770                expression_id: "expr-certainty".to_string(),
771                facts: StringTypeFactsV2 {
772                    kind: "exact".to_string(),
773                    constraint_kind: None,
774                    values: Some(vec!["x".to_string()]),
775                    prefix: None,
776                    suffix: None,
777                    min_len: None,
778                    max_len: None,
779                    char_must: None,
780                    char_may: None,
781                    may_include_other_chars: None,
782                    provenance: None,
783                },
784                control_flow_graph: Some(engine_input_producers::TypeFactControlFlowGraphV2 {
785                    entry_block_id: "seed".to_string(),
786                    blocks: vec![
787                        engine_input_producers::TypeFactControlFlowBlockV2 {
788                            id: "seed".to_string(),
789                            kind: "assignment".to_string(),
790                            transfer_kind: "assignFacts".to_string(),
791                            successor_block_ids: vec!["loop".to_string()],
792                            symbol_ordinal: None,
793                            variable_name: None,
794                            expression_kind: None,
795                            boundary_effect: "unknownBoundary".to_string(),
796                            facts: Some(StringTypeFactsV2 {
797                                kind: "finiteSet".to_string(),
798                                constraint_kind: None,
799                                values: Some(vec!["a".to_string(), "b".to_string()]),
800                                prefix: None,
801                                suffix: None,
802                                min_len: None,
803                                max_len: None,
804                                char_must: None,
805                                char_may: None,
806                                may_include_other_chars: None,
807                                provenance: None,
808                            }),
809                        },
810                        engine_input_producers::TypeFactControlFlowBlockV2 {
811                            id: "loop".to_string(),
812                            kind: "loop".to_string(),
813                            transfer_kind: "concatFacts".to_string(),
814                            successor_block_ids: vec!["loop".to_string()],
815                            symbol_ordinal: None,
816                            variable_name: None,
817                            expression_kind: None,
818                            boundary_effect: "unknownBoundary".to_string(),
819                            facts: None,
820                        },
821                    ],
822                }),
823            }],
824        }
825    }
826
827    fn selector_certainty_colon_collision_input() -> EngineInputV2 {
828        let mut input = selector_certainty_product_input();
829        input.sources[0].document.class_expressions[0].id = "b:c".to_string();
830        input.sources[0].document.class_expressions[0].class_name = Some("x".to_string());
831        input.sources[0].document.class_expressions[0].access_path =
832            Some(vec!["styles".to_string(), "x".to_string()]);
833        let second_expression = ClassExpressionInputV2 {
834            id: "c".to_string(),
835            kind: "styleAccess".to_string(),
836            scss_module_path: "/tmp/App.module.scss".to_string(),
837            range: input.sources[0].document.class_expressions[0].range.clone(),
838            class_name: Some("y".to_string()),
839            root_binding_decl_id: None,
840            access_path: Some(vec!["styles".to_string(), "y".to_string()]),
841        };
842        input.sources[0]
843            .document
844            .class_expressions
845            .push(second_expression);
846
847        let second_selector = StyleSelectorV2 {
848            name: "y".to_string(),
849            view_kind: "canonical".to_string(),
850            canonical_name: Some("y".to_string()),
851            range: input.styles[0].document.selectors[0].range.clone(),
852            nested_safety: Some("safe".to_string()),
853            composes: None,
854            bem_suffix: None,
855        };
856        input.styles[0].document.selectors.push(second_selector);
857
858        let mut first_fact = input.type_facts[0].clone();
859        first_fact.file_path = "/tmp/A".to_string();
860        first_fact.expression_id = "b:c".to_string();
861        let mut second_fact = first_fact.clone();
862        second_fact.file_path = "/tmp/A:b".to_string();
863        second_fact.expression_id = "c".to_string();
864        second_fact.facts.values = Some(vec!["y".to_string()]);
865        if let Some(second_graph) = second_fact.control_flow_graph.as_mut() {
866            second_graph.blocks.truncate(1);
867            second_graph.blocks[0].successor_block_ids.clear();
868        }
869        input.type_facts = vec![first_fact, second_fact];
870        input
871    }
872
873    #[test]
874    fn expression_domain_runtime_reuses_graph_databases_across_revisions() {
875        let input = EngineInputV2 {
876            version: "core-runtime".to_string(),
877            sources: Vec::new(),
878            styles: Vec::new(),
879            type_facts: Vec::new(),
880        };
881        let mut runtime = OmenaQueryExpressionDomainFlowRuntimeV0::default();
882
883        let first =
884            summarize_omena_query_expression_domain_incremental_flow_analysis(&input, &mut runtime);
885        let second =
886            summarize_omena_query_expression_domain_incremental_flow_analysis(&input, &mut runtime);
887
888        assert_eq!(first.revision, 1);
889        assert_eq!(second.revision, 2);
890        assert_eq!(runtime.revision(), 2);
891    }
892
893    #[test]
894    fn nonconverged_flow_hedge_demotes_typed_query_projection() {
895        let input = selector_certainty_product_input();
896        let graph_id = "/tmp/App.tsx:expr-certainty:expression-domain-control-flow";
897        let control_flow = summarize_omena_query_expression_domain_control_flow_analysis(&input);
898        let projection = summarize_omena_query_expression_domain_selector_projection(&input);
899        let control_entry = &control_flow.analyses[0];
900        let entry = &projection.projections[0];
901
902        println!(
903            "certainty-hedge-census graphId={graph_id} hedged=1 base=exact certainty=possible"
904        );
905        assert_eq!(control_entry.graph_id, graph_id);
906        assert!(!control_entry.analysis.flow_analysis.converged);
907        assert!(
908            control_entry
909                .analysis
910                .flow_analysis
911                .nodes
912                .iter()
913                .all(|node| {
914                    matches!(
915                        &node.value,
916                        AbstractClassValueV0::Top {
917                            provenance: Some(AbstractClassValueProvenanceV0::FlowIterationLimit)
918                        }
919                    )
920                })
921        );
922        assert_eq!(entry.value_kind, "exact");
923        assert_eq!(entry.selector_names, vec!["x".to_string()]);
924        assert_eq!(entry.certainty, SelectorProjectionCertaintyV0::Possible);
925    }
926
927    #[test]
928    fn colon_colliding_graph_ids_bind_selector_certainty_by_type_fact_tuple() {
929        let input = selector_certainty_colon_collision_input();
930        let control_flow = summarize_omena_query_expression_domain_control_flow_analysis(&input);
931        let projection = summarize_omena_query_expression_domain_selector_projection(&input);
932
933        assert_eq!(control_flow.analyses.len(), 2);
934        assert_eq!(
935            control_flow.analyses[0].graph_id, control_flow.analyses[1].graph_id,
936            "fixture must retain the diagnostic graph-id collision"
937        );
938        let nonconverged = projection
939            .projections
940            .iter()
941            .filter(|entry| entry.file_path == "/tmp/A" && entry.node_id == "b:c")
942            .collect::<Vec<_>>();
943        let converged = projection
944            .projections
945            .iter()
946            .filter(|entry| entry.file_path == "/tmp/A:b" && entry.node_id == "c")
947            .collect::<Vec<_>>();
948
949        assert_eq!(nonconverged.len(), 1);
950        assert_eq!(converged.len(), 1);
951        assert_eq!(
952            nonconverged[0].certainty,
953            SelectorProjectionCertaintyV0::Possible
954        );
955        assert_eq!(converged[0].certainty, SelectorProjectionCertaintyV0::Exact);
956    }
957
958    #[test]
959    #[should_panic(expected = "selector-certainty flow hedge duplicate type-fact key")]
960    fn duplicate_selector_certainty_type_fact_key_fails_closed() {
961        let mut input = selector_certainty_product_input();
962        input.type_facts.push(input.type_facts[0].clone());
963
964        let _ = summarize_omena_query_expression_domain_selector_projection(&input);
965    }
966
967    #[test]
968    #[should_panic(
969        expected = "selector-certainty flow hedge requires one control analysis per control-flow type fact"
970    )]
971    fn missing_selector_certainty_control_analysis_fails_closed() {
972        let input = selector_certainty_product_input();
973        let mut control_flow =
974            summarize_omena_query_expression_domain_control_flow_analysis(&input);
975        control_flow.analyses.clear();
976
977        let _ = bind_expression_domain_selector_certainty_flow_hedges(&input, control_flow);
978    }
979
980    #[test]
981    fn analysis_precision_view_maps_known_producers_and_fails_closed() {
982        let precision = |value_domain: &str| OmenaQueryAnalysisPrecisionV0 {
983            product: "omena-query.analysis-precision".to_string(),
984            value_domain: value_domain.to_string(),
985            flow_sensitivity: "fixture".to_string(),
986            context_sensitivity: "fixture".to_string(),
987            revision_axis: "fixture".to_string(),
988        };
989
990        assert_eq!(
991            fact_precision_from_analysis_precision(&precision("cascadeAtPosition")),
992            FactPrecision::Exact
993        );
994        assert_eq!(
995            fact_precision_from_analysis_precision(&precision("styleModuleResolution")),
996            FactPrecision::Exact
997        );
998        assert_eq!(
999            fact_precision_from_analysis_precision(&precision("classValueResolution")),
1000            FactPrecision::Conservative
1001        );
1002        assert_eq!(
1003            fact_precision_from_analysis_precision(&precision("classValueUniverse")),
1004            FactPrecision::Conservative
1005        );
1006        assert_eq!(
1007            fact_precision_from_analysis_precision(&precision("classValueFlow")),
1008            FactPrecision::Heuristic
1009        );
1010        assert_eq!(
1011            fact_precision_from_analysis_precision(&precision("unknown")),
1012            FactPrecision::Unknown
1013        );
1014        assert_eq!(
1015            fact_precision_from_analysis_precision(&precision("unregistered")),
1016            FactPrecision::Unknown
1017        );
1018    }
1019}