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, ClassValueFlowSealedIncrementalAnalysisV0,
35    SealedClassValueFlowAnalysisArtifactV0, analyze_class_value_flow_incremental,
36    analyze_class_value_flow_incremental_with_artifact, class_value_flow_incremental_input,
37    project_abstract_value_selectors, summarize_omena_abstract_value_domain,
38    summarize_reduced_class_value_product,
39};
40pub use omena_abstract_value::{
41    AbstractClassValueV0, AbstractPropertyValueCandidateV0, AbstractPropertyValueNarrowingV0,
42    AbstractPropertyValueV0, AbstractValueDomainSummaryV0, CascadeContextV0,
43    CascadeValueFamilyMemberV0, ClassBoundaryEffectV0, ClassValueFlowAnalysisV0,
44    ClassValueFlowIncrementalAnalysisV0, CssValueValidationClassV0, ExternalStringTypeFactsV0,
45    FactPrecision, FirstWitnessErrorV0, GuardAtomV0, GuardedTokenInputV0, GuardedTokenLanguageV0,
46    GuardedTokenMapInputV0, GuardedTokenMapV0, GuardedTokenObserverV0, Lin01ProvenanceSemiringV0,
47    LinearProvenancePathV0, LinearProvenanceV0, NaturalCountProvenanceSemiringV0,
48    OmenaAbstractValueCoverageDirectionV0, OmenaAbstractValuePrecisionBasisV0,
49    OmenaAbstractValuePrecisionWitnessV0, PolynomialProvenanceProjectionV0,
50    PolynomialProvenanceTermV0, PolynomialProvenanceV0, PolynomialProvenanceVariableV0,
51    ProvenanceSemiringLawReportV0, ReducedClassValueProductIterationV0, ReducedClassValueProductV0,
52    SelectorProjectionCertaintyV0, SpecStandardPropertyValueValidatorV0, TokenObserverProjectionV0,
53    abstract_class_value_from_facts, abstract_class_value_kind,
54    derive_context_indexed_cascade_restriction_maps_v0, fact_precision_from_class_value,
55    fact_precision_from_class_value_with_witness, iterate_reduced_class_value_product_constraints,
56    join_abstract_class_values, narrow_abstract_property_value_for_authored_cascade_branch,
57    narrow_abstract_property_value_for_cascade_branch,
58    narrow_abstract_property_value_for_pseudo_state, prefix_suffix_class_value,
59    summarize_context_indexed_cascade_value_family_v0,
60    summarize_polynomial_provenance_from_linear_v0, top_class_value,
61    validate_registered_property_value_v0, validate_standard_property_value_v0,
62    verify_provenance_semiring_laws_on_fixtures,
63};
64#[allow(deprecated)]
65#[deprecated(
66    since = "0.4.0",
67    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"
68)]
69pub use omena_abstract_value::{
70    derive_cascade_restriction_maps_v0, summarize_cascade_value_family_v0,
71};
72pub use omena_incremental::{
73    IncrementalEditDistancePriorityInputV0, IncrementalGraphInputV0,
74    IncrementalInvalidationPriorityPlanV0, IncrementalNodeInputV0, IncrementalRevisionV0,
75    OmenaIncrementalDatabaseV0, OmenaSalsaDatabaseV0, OmenaWorkspaceSnapshotIdV0,
76    snapshot_from_graph_input,
77};
78pub use omena_refinement::{
79    CascadeDimensionalRefinementBridgeV0, RefinementPropertyPredicateV0,
80    summarize_cascade_dimensional_refinement_bridge_v0,
81};
82pub use omena_resolver::OmenaResolverSourceResolutionRuntimeIndexV0;
83use omena_resolver::{
84    summarize_omena_resolver_canonical_producer_signal, summarize_omena_resolver_query_fragments,
85    summarize_omena_resolver_source_resolution_runtime,
86};
87pub use omena_value_lattice::{
88    canonicalize_css_value, split_top_level_value_arguments,
89    split_top_level_whitespace_value_components,
90};
91use serde::{Deserialize, Serialize};
92
93pub const OMENA_QUERY_CURRENT_SCHEMA_VERSION: &str = "0";
94pub const OMENA_QUERY_CURRENT_SCHEMA_VERSION_LABEL: &str = "V0";
95
96#[cfg(feature = "test-support")]
97thread_local! {
98    static SELECTOR_PROJECTION_EVALUATION_COUNT: Cell<usize> = const { Cell::new(0) };
99}
100
101#[cfg(feature = "test-support")]
102pub fn reset_selector_projection_evaluation_count_for_test() {
103    SELECTOR_PROJECTION_EVALUATION_COUNT.with(|counter| counter.set(0));
104}
105
106#[cfg(feature = "test-support")]
107pub fn selector_projection_evaluation_count_for_test() -> usize {
108    SELECTOR_PROJECTION_EVALUATION_COUNT.with(Cell::get)
109}
110
111#[cfg(feature = "test-support")]
112fn record_selector_projection_evaluation_for_test() {
113    SELECTOR_PROJECTION_EVALUATION_COUNT.with(|counter| counter.set(counter.get() + 1));
114}
115
116#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
117#[serde(rename_all = "camelCase")]
118pub struct OmenaQueryAnalysisPrecisionV0 {
119    pub product: String,
120    pub value_domain: String,
121    pub flow_sensitivity: String,
122    pub context_sensitivity: String,
123    pub revision_axis: String,
124}
125
126const OMENA_QUERY_ANALYSIS_FACT_PRECISION_BY_VALUE_DOMAIN: &[(&str, FactPrecision)] = &[
127    ("cascadeAtPosition", FactPrecision::Exact),
128    ("styleModuleResolution", FactPrecision::Exact),
129    ("classValueResolution", FactPrecision::Conservative),
130    ("classValueUniverse", FactPrecision::Conservative),
131    ("classValueFlow", FactPrecision::Heuristic),
132    ("unknown", FactPrecision::Unknown),
133];
134
135pub fn fact_precision_from_analysis_precision(
136    precision: &OmenaQueryAnalysisPrecisionV0,
137) -> FactPrecision {
138    OMENA_QUERY_ANALYSIS_FACT_PRECISION_BY_VALUE_DOMAIN
139        .iter()
140        .find_map(|(value_domain, mapped)| {
141            (*value_domain == precision.value_domain).then_some(*mapped)
142        })
143        .unwrap_or(FactPrecision::Unknown)
144}
145
146#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
147#[serde(rename_all = "camelCase")]
148pub struct OmenaQueryAnalysisResultV0<TValue> {
149    pub schema_version: String,
150    pub product: String,
151    pub value: TValue,
152    pub precision: OmenaQueryAnalysisPrecisionV0,
153    pub provenance: Vec<String>,
154    pub revision: u64,
155}
156
157impl<TValue> OmenaQueryAnalysisResultV0<TValue> {
158    pub fn new(
159        value: TValue,
160        precision: OmenaQueryAnalysisPrecisionV0,
161        provenance: Vec<String>,
162        revision: u64,
163    ) -> Self {
164        Self {
165            schema_version: OMENA_QUERY_CURRENT_SCHEMA_VERSION.to_string(),
166            product: "omena-query.analysis-result".to_string(),
167            value,
168            precision,
169            provenance,
170            revision,
171        }
172    }
173}
174
175#[derive(Debug, Serialize)]
176#[serde(rename_all = "camelCase")]
177pub struct OmenaQueryFragmentBundleV0 {
178    pub schema_version: &'static str,
179    pub product: &'static str,
180    pub input_version: String,
181    pub expression_semantics: ExpressionSemanticsQueryFragmentsV0,
182    pub source_resolution: SourceResolutionQueryFragmentsV0,
183    pub selector_usage: SelectorUsageQueryFragmentsV0,
184}
185
186#[derive(Debug, Serialize)]
187#[serde(rename_all = "camelCase")]
188pub struct OmenaQueryExpressionDomainIncrementalFlowAnalysisV0 {
189    pub schema_version: &'static str,
190    pub product: &'static str,
191    pub input_version: String,
192    pub revision: u64,
193    pub graph_count: usize,
194    pub dirty_graph_count: usize,
195    pub reused_graph_count: usize,
196    pub analyses: Vec<OmenaQueryExpressionDomainIncrementalFlowAnalysisEntryV0>,
197}
198
199#[derive(Debug, Serialize)]
200#[serde(rename_all = "camelCase")]
201pub struct OmenaQueryExpressionDomainIncrementalFlowAnalysisEntryV0 {
202    pub graph_id: String,
203    pub file_path: String,
204    pub analysis: ClassValueFlowIncrementalAnalysisV0,
205}
206
207#[derive(Debug, Serialize)]
208#[serde(rename_all = "camelCase")]
209pub struct OmenaQueryExpressionDomainSelectorProjectionV0 {
210    pub schema_version: &'static str,
211    pub product: &'static str,
212    pub input_version: String,
213    pub projection_count: usize,
214    pub projections: Vec<OmenaQueryExpressionDomainSelectorProjectionEntryV0>,
215}
216
217#[derive(Debug, Serialize)]
218#[serde(rename_all = "camelCase")]
219pub struct OmenaQueryExpressionDomainSelectorProjectionEntryV0 {
220    pub graph_id: String,
221    pub file_path: String,
222    pub node_id: String,
223    pub target_style_paths: Vec<String>,
224    pub value_kind: &'static str,
225    #[serde(skip_serializing_if = "Option::is_none")]
226    pub reduced_product: Option<ReducedClassValueProductV0>,
227    pub selector_names: Vec<String>,
228    pub certainty: SelectorProjectionCertaintyV0,
229}
230
231#[derive(Debug, Clone, PartialEq, Eq, Serialize)]
232#[serde(rename_all = "camelCase")]
233pub struct OmenaQueryExpressionDomainSelectorPrecisionV0 {
234    pub graph_id: String,
235    pub node_id: String,
236    pub precision: FactPrecision,
237}
238
239#[derive(Debug, Clone, PartialEq, Eq)]
240struct ExpressionDomainSelectorCertaintyFlowHedgeV0 {
241    graph_converged: bool,
242    contains_flow_iteration_limit: bool,
243}
244
245#[derive(Default)]
246pub struct OmenaQueryExpressionDomainFlowRuntimeV0 {
247    revision: u64,
248    artifacts_by_graph_id: BTreeMap<String, SealedClassValueFlowAnalysisArtifactV0>,
249    read_set_digest_check_count: usize,
250    analysis_rebuild_count: usize,
251    artifact_refusal_count: usize,
252}
253
254impl OmenaQueryExpressionDomainFlowRuntimeV0 {
255    pub fn revision(&self) -> u64 {
256        self.revision
257    }
258
259    pub fn graph_count(&self) -> usize {
260        self.artifacts_by_graph_id.len()
261    }
262
263    /// Number of sealed read-set digest checks performed by the product
264    /// runtime across all revisions.
265    pub fn read_set_digest_check_count(&self) -> usize {
266        self.read_set_digest_check_count
267    }
268
269    /// Number of analyses rebuilt by the product runtime across all revisions.
270    pub fn analysis_rebuild_count(&self) -> usize {
271        self.analysis_rebuild_count
272    }
273
274    /// Number of internally retained artifacts refused before a safe rebuild.
275    pub fn artifact_refusal_count(&self) -> usize {
276        self.artifact_refusal_count
277    }
278
279    pub fn analyze_input(
280        &mut self,
281        input: &EngineInputV2,
282    ) -> OmenaQueryExpressionDomainIncrementalFlowAnalysisV0 {
283        self.analyze_input_with_artifact_analyzer(
284            input,
285            analyze_class_value_flow_incremental_with_artifact,
286        )
287    }
288
289    fn analyze_input_with_artifact_analyzer<F>(
290        &mut self,
291        input: &EngineInputV2,
292        mut analyze_artifact: F,
293    ) -> OmenaQueryExpressionDomainIncrementalFlowAnalysisV0
294    where
295        F: FnMut(
296            &omena_abstract_value::ClassValueFlowGraphV0,
297            Option<&SealedClassValueFlowAnalysisArtifactV0>,
298            u64,
299        ) -> Result<
300            ClassValueFlowSealedIncrementalAnalysisV0,
301            omena_abstract_value::ClassValueFlowArtifactRefusalV0,
302        >,
303    {
304        self.revision += 1;
305        let revision = self.revision;
306        let flow_graphs = collect_expression_domain_flow_graphs(input);
307        let live_graph_ids = flow_graphs
308            .iter()
309            .map(|entry| entry.graph_id.clone())
310            .collect::<BTreeSet<_>>();
311
312        self.artifacts_by_graph_id
313            .retain(|graph_id, _| live_graph_ids.contains(graph_id));
314
315        let analyses = flow_graphs
316            .into_iter()
317            .map(|entry| {
318                let previous_artifact = self.artifacts_by_graph_id.get(&entry.graph_id);
319                let sealed_analysis = analyze_artifact(&entry.graph, previous_artifact, revision);
320                let (analysis, next_artifact) = match sealed_analysis {
321                    Ok(sealed_analysis) => {
322                        self.read_set_digest_check_count = self
323                            .read_set_digest_check_count
324                            .saturating_add(sealed_analysis.read_set_digest_check_count);
325                        self.analysis_rebuild_count = self
326                            .analysis_rebuild_count
327                            .saturating_add(sealed_analysis.analysis_rebuild_count);
328                        let (analysis, next_artifact) = legacy_flow_analysis_from_sealed(
329                            &entry.graph,
330                            revision,
331                            previous_artifact,
332                            sealed_analysis,
333                        );
334                        (analysis, Some(next_artifact))
335                    }
336                    Err(_) => {
337                        self.read_set_digest_check_count = self
338                            .read_set_digest_check_count
339                            .saturating_add(usize::from(previous_artifact.is_some()));
340                        self.analysis_rebuild_count = self.analysis_rebuild_count.saturating_add(1);
341                        self.artifact_refusal_count = self.artifact_refusal_count.saturating_add(1);
342                        (
343                            analyze_class_value_flow_incremental(&entry.graph, None, revision),
344                            None,
345                        )
346                    }
347                };
348                if let Some(next_artifact) = next_artifact {
349                    self.artifacts_by_graph_id
350                        .insert(entry.graph_id.clone(), next_artifact);
351                } else {
352                    self.artifacts_by_graph_id.remove(&entry.graph_id);
353                }
354
355                OmenaQueryExpressionDomainIncrementalFlowAnalysisEntryV0 {
356                    graph_id: entry.graph_id,
357                    file_path: entry.file_path,
358                    analysis,
359                }
360            })
361            .collect::<Vec<_>>();
362
363        let dirty_graph_count = analyses
364            .iter()
365            .filter(|entry| entry.analysis.incremental_plan.dirty_node_count > 0)
366            .count();
367        let reused_graph_count = analyses
368            .iter()
369            .filter(|entry| entry.analysis.reused_previous_analysis)
370            .count();
371
372        OmenaQueryExpressionDomainIncrementalFlowAnalysisV0 {
373            schema_version: OMENA_QUERY_CURRENT_SCHEMA_VERSION,
374            product: "omena-query.expression-domain-incremental-flow-analysis",
375            input_version: input.version.clone(),
376            revision,
377            graph_count: analyses.len(),
378            dirty_graph_count,
379            reused_graph_count,
380            analyses,
381        }
382    }
383}
384
385fn legacy_flow_analysis_from_sealed(
386    graph: &omena_abstract_value::ClassValueFlowGraphV0,
387    revision: u64,
388    previous_artifact: Option<&SealedClassValueFlowAnalysisArtifactV0>,
389    sealed: ClassValueFlowSealedIncrementalAnalysisV0,
390) -> (
391    ClassValueFlowIncrementalAnalysisV0,
392    SealedClassValueFlowAnalysisArtifactV0,
393) {
394    let ClassValueFlowSealedIncrementalAnalysisV0 {
395        reused_previous_analysis,
396        incremental_plan,
397        analysis,
398        next_artifact,
399        ..
400    } = sealed;
401    let (incremental_plan, next_snapshot) = match incremental_plan {
402        Some(incremental_plan) => (incremental_plan, next_artifact.snapshot().clone()),
403        None => {
404            // The public query product retains its established plan/snapshot
405            // JSON values. Reconstruct from the prior verified snapshot: the
406            // sealed reuse artifact has already advanced its top-level
407            // revision, which would otherwise re-stamp unchanged nodes at the
408            // current revision instead of preserving the legacy N-1 stamp.
409            // This projection rebuild does not rerun flow analysis; its
410            // residual cost is distinct from the sealed helper benchmark.
411            let mut database = OmenaIncrementalDatabaseV0::default();
412            if let Some(previous_artifact) = previous_artifact {
413                database.restore_snapshot(previous_artifact.snapshot());
414            }
415            let update = database
416                .plan_and_upsert_graph_input(&class_value_flow_incremental_input(graph, revision));
417            (update.incremental_plan, update.next_snapshot)
418        }
419    };
420    (
421        ClassValueFlowIncrementalAnalysisV0 {
422            schema_version: "0",
423            product: "omena-abstract-value.incremental-flow-analysis",
424            reused_previous_analysis,
425            incremental_plan,
426            next_snapshot,
427            analysis,
428        },
429        next_artifact,
430    )
431}
432
433pub fn summarize_omena_query_core_abstract_value_domain() -> AbstractValueDomainSummaryV0 {
434    summarize_omena_abstract_value_domain()
435}
436
437pub fn summarize_omena_query_fragment_bundle(input: &EngineInputV2) -> OmenaQueryFragmentBundleV0 {
438    OmenaQueryFragmentBundleV0 {
439        schema_version: OMENA_QUERY_CURRENT_SCHEMA_VERSION,
440        product: "omena-query.fragment-bundle",
441        input_version: input.version.clone(),
442        expression_semantics: summarize_omena_query_expression_semantics_query_fragments(input),
443        source_resolution: summarize_omena_query_source_resolution_query_fragments(input),
444        selector_usage: summarize_omena_query_selector_usage_query_fragments(input),
445    }
446}
447
448pub fn summarize_omena_query_expression_semantics_query_fragments(
449    input: &EngineInputV2,
450) -> ExpressionSemanticsQueryFragmentsV0 {
451    summarize_expression_semantics_query_fragments_input(input)
452}
453
454pub fn summarize_omena_query_expression_domain_flow_analysis(
455    input: &EngineInputV2,
456) -> ExpressionDomainFlowAnalysisV0 {
457    summarize_expression_domain_flow_analysis_input(input)
458}
459
460pub fn summarize_omena_query_expression_domain_control_flow_analysis(
461    input: &EngineInputV2,
462) -> ExpressionDomainControlFlowAnalysisV0 {
463    summarize_expression_domain_control_flow_analysis_input(input)
464}
465
466pub fn summarize_omena_query_expression_domain_call_site_flow_analysis(
467    input: &EngineInputV2,
468) -> ExpressionDomainCallSiteFlowAnalysisV0 {
469    summarize_expression_domain_call_site_flow_analysis_input(input)
470}
471
472pub fn summarize_omena_query_expression_domain_provenance_explanations(
473    input: &EngineInputV2,
474) -> ExpressionDomainProvenanceExplanationsV0 {
475    summarize_expression_domain_provenance_explanations_input(input)
476}
477
478pub fn summarize_omena_query_expression_domain_reduced_product_iteration(
479    input: &EngineInputV2,
480) -> ExpressionDomainReducedProductIterationV0 {
481    summarize_expression_domain_reduced_product_iteration_input(input)
482}
483
484pub fn summarize_omena_query_expression_domain_incremental_flow_analysis(
485    input: &EngineInputV2,
486    runtime: &mut OmenaQueryExpressionDomainFlowRuntimeV0,
487) -> OmenaQueryExpressionDomainIncrementalFlowAnalysisV0 {
488    runtime.analyze_input(input)
489}
490
491pub fn summarize_omena_query_expression_domain_incremental_flow_analysis_result(
492    input: &EngineInputV2,
493    runtime: &mut OmenaQueryExpressionDomainFlowRuntimeV0,
494) -> OmenaQueryAnalysisResultV0<OmenaQueryExpressionDomainIncrementalFlowAnalysisV0> {
495    let value = runtime.analyze_input(input);
496    let revision = value.revision;
497    OmenaQueryAnalysisResultV0::new(
498        value,
499        OmenaQueryAnalysisPrecisionV0 {
500            product: "omena-query.analysis-precision".to_string(),
501            value_domain: "classValueFlow".to_string(),
502            flow_sensitivity: "incrementalDataflow".to_string(),
503            context_sensitivity: "perExpressionGraph".to_string(),
504            revision_axis: "OmenaQueryExpressionDomainFlowRuntimeV0.revision".to_string(),
505        },
506        vec![
507            "omena-query-core.expression-domain-runtime".to_string(),
508            "omena-abstract-value.incremental-class-value-flow".to_string(),
509        ],
510        revision,
511    )
512}
513
514pub fn summarize_omena_query_expression_domain_selector_projection(
515    input: &EngineInputV2,
516) -> OmenaQueryExpressionDomainSelectorProjectionV0 {
517    summarize_omena_query_expression_domain_selector_projection_with_precision(input).0
518}
519
520pub fn summarize_omena_query_expression_domain_selector_projection_with_precision(
521    input: &EngineInputV2,
522) -> (
523    OmenaQueryExpressionDomainSelectorProjectionV0,
524    Vec<OmenaQueryExpressionDomainSelectorPrecisionV0>,
525) {
526    summarize_omena_query_expression_domain_selector_projection_with_precision_and_style_path_resolver(
527        input,
528        |target, _known| Some(target.to_string()),
529    )
530}
531
532pub fn summarize_omena_query_expression_domain_selector_projection_with_precision_and_style_path_resolver<
533    F,
534>(
535    input: &EngineInputV2,
536    resolve_style_path: F,
537) -> (
538    OmenaQueryExpressionDomainSelectorProjectionV0,
539    Vec<OmenaQueryExpressionDomainSelectorPrecisionV0>,
540)
541where
542    F: Fn(&str, &[String]) -> Option<String>,
543{
544    #[cfg(feature = "test-support")]
545    record_selector_projection_evaluation_for_test();
546    let selector_certainty_flow_hedges = expression_domain_selector_certainty_flow_hedges(input);
547    summarize_omena_query_expression_domain_selector_projection_with_flow_hedges(
548        input,
549        resolve_style_path,
550        &selector_certainty_flow_hedges,
551    )
552}
553
554fn summarize_omena_query_expression_domain_selector_projection_with_flow_hedges<F>(
555    input: &EngineInputV2,
556    resolve_style_path: F,
557    selector_certainty_flow_hedges: &BTreeMap<
558        (String, String),
559        ExpressionDomainSelectorCertaintyFlowHedgeV0,
560    >,
561) -> (
562    OmenaQueryExpressionDomainSelectorProjectionV0,
563    Vec<OmenaQueryExpressionDomainSelectorPrecisionV0>,
564)
565where
566    F: Fn(&str, &[String]) -> Option<String>,
567{
568    let style_selectors_by_path = style_selector_universe_by_path(input);
569    let known_style_paths = style_selectors_by_path.keys().cloned().collect::<Vec<_>>();
570    let expression_targets = expression_target_style_paths(input);
571    let flow_analysis = summarize_omena_query_expression_domain_flow_analysis(input);
572    let mut projections = Vec::new();
573    let mut precisions = Vec::new();
574
575    for graph in flow_analysis.analyses {
576        for node in graph.analysis.nodes {
577            let target_style_paths = target_style_paths_for_flow_node(
578                node.id.as_str(),
579                node.predecessor_ids.as_slice(),
580                &expression_targets,
581            );
582            let resolved_target_style_paths = target_style_paths
583                .iter()
584                .map(|path| resolve_style_path(path, known_style_paths.as_slice()))
585                .collect::<Option<Vec<_>>>();
586            let selector_universe = selector_universe_for_targets(
587                resolved_target_style_paths.as_deref(),
588                &style_selectors_by_path,
589            );
590            let projection = project_abstract_value_selectors(&node.value, &selector_universe);
591            let certainty = hedge_selector_projection_certainty(
592                projection.certainty,
593                selector_certainty_flow_hedges.get(&(graph.file_path.clone(), node.id.clone())),
594            );
595            precisions.push(OmenaQueryExpressionDomainSelectorPrecisionV0 {
596                graph_id: graph.graph_id.clone(),
597                node_id: node.id.clone(),
598                precision: fact_precision_from_class_value(&node.value),
599            });
600            projections.push(OmenaQueryExpressionDomainSelectorProjectionEntryV0 {
601                graph_id: graph.graph_id.clone(),
602                file_path: graph.file_path.clone(),
603                node_id: node.id,
604                target_style_paths,
605                value_kind: node.value_kind,
606                reduced_product: summarize_reduced_class_value_product(&node.value),
607                selector_names: projection.selector_names,
608                certainty,
609            });
610        }
611    }
612
613    (
614        OmenaQueryExpressionDomainSelectorProjectionV0 {
615            schema_version: OMENA_QUERY_CURRENT_SCHEMA_VERSION,
616            product: "omena-query.expression-domain-selector-projection",
617            input_version: input.version.clone(),
618            projection_count: projections.len(),
619            projections,
620        },
621        precisions,
622    )
623}
624
625fn expression_domain_selector_certainty_flow_hedges(
626    input: &EngineInputV2,
627) -> BTreeMap<(String, String), ExpressionDomainSelectorCertaintyFlowHedgeV0> {
628    bind_expression_domain_selector_certainty_flow_hedges(
629        input,
630        summarize_omena_query_expression_domain_control_flow_analysis(input),
631    )
632}
633
634fn bind_expression_domain_selector_certainty_flow_hedges(
635    input: &EngineInputV2,
636    control_flow: ExpressionDomainControlFlowAnalysisV0,
637) -> BTreeMap<(String, String), ExpressionDomainSelectorCertaintyFlowHedgeV0> {
638    let control_flow_facts = input
639        .type_facts
640        .iter()
641        .filter(|entry| entry.control_flow_graph.is_some())
642        .collect::<Vec<_>>();
643    assert_eq!(
644        control_flow_facts.len(),
645        control_flow.analyses.len(),
646        "selector-certainty flow hedge requires one control analysis per control-flow type fact"
647    );
648
649    let mut hedges = BTreeMap::new();
650    for (entry, analyzed) in control_flow_facts.into_iter().zip(control_flow.analyses) {
651        let diagnostic_graph_id = format!(
652            "{}:{}:expression-domain-control-flow",
653            entry.file_path, entry.expression_id
654        );
655        assert_eq!(
656            analyzed.file_path, entry.file_path,
657            "selector-certainty flow hedge control analysis order/file mismatch"
658        );
659        assert_eq!(
660            analyzed.graph_id, diagnostic_graph_id,
661            "selector-certainty flow hedge control analysis order/graph mismatch"
662        );
663
664        let contains_flow_iteration_limit = analyzed
665            .analysis
666            .flow_analysis
667            .nodes
668            .iter()
669            .any(|node| abstract_value_contains_flow_iteration_limit(&node.value));
670        let key = (entry.file_path.clone(), entry.expression_id.clone());
671        let previous = hedges.insert(
672            key.clone(),
673            ExpressionDomainSelectorCertaintyFlowHedgeV0 {
674                graph_converged: analyzed.analysis.flow_analysis.converged,
675                contains_flow_iteration_limit,
676            },
677        );
678        assert!(
679            previous.is_none(),
680            "selector-certainty flow hedge duplicate type-fact key: file_path={:?} expression_id={:?}",
681            key.0,
682            key.1
683        );
684    }
685
686    hedges
687}
688
689fn hedge_selector_projection_certainty(
690    base: SelectorProjectionCertaintyV0,
691    flow_hedge: Option<&ExpressionDomainSelectorCertaintyFlowHedgeV0>,
692) -> SelectorProjectionCertaintyV0 {
693    flow_hedge.map_or(base, |hedge| {
694        hedge_selector_certainty_for_flow(
695            base,
696            hedge.graph_converged,
697            hedge.contains_flow_iteration_limit,
698        )
699    })
700}
701
702fn hedge_selector_certainty_for_flow(
703    base: SelectorProjectionCertaintyV0,
704    graph_converged: bool,
705    contains_flow_iteration_limit: bool,
706) -> SelectorProjectionCertaintyV0 {
707    if graph_converged && !contains_flow_iteration_limit {
708        base
709    } else {
710        SelectorProjectionCertaintyV0::Possible
711    }
712}
713
714fn abstract_value_contains_flow_iteration_limit(value: &AbstractClassValueV0) -> bool {
715    let provenance = match value {
716        AbstractClassValueV0::Automaton { provenance, .. }
717        | AbstractClassValueV0::Prefix { provenance, .. }
718        | AbstractClassValueV0::Suffix { provenance, .. }
719        | AbstractClassValueV0::PrefixSuffix { provenance, .. }
720        | AbstractClassValueV0::CharInclusion { provenance, .. }
721        | AbstractClassValueV0::Composite { provenance, .. }
722        | AbstractClassValueV0::Top { provenance } => *provenance,
723        AbstractClassValueV0::Bottom
724        | AbstractClassValueV0::Exact { .. }
725        | AbstractClassValueV0::FiniteSet { .. } => None,
726    };
727
728    provenance == Some(AbstractClassValueProvenanceV0::FlowIterationLimit)
729}
730
731fn expression_target_style_paths(input: &EngineInputV2) -> BTreeMap<String, String> {
732    input
733        .sources
734        .iter()
735        .flat_map(|source| source.document.class_expressions.iter())
736        .map(|expression| (expression.id.clone(), expression.scss_module_path.clone()))
737        .collect()
738}
739
740fn style_selector_universe_by_path(input: &EngineInputV2) -> BTreeMap<String, Vec<String>> {
741    input
742        .styles
743        .iter()
744        .map(|style| {
745            let selector_names = style
746                .document
747                .selectors
748                .iter()
749                .map(|selector| {
750                    selector
751                        .canonical_name
752                        .clone()
753                        .unwrap_or_else(|| selector.name.clone())
754                })
755                .collect::<BTreeSet<_>>()
756                .into_iter()
757                .collect::<Vec<_>>();
758            (style.file_path.clone(), selector_names)
759        })
760        .collect()
761}
762
763fn target_style_paths_for_flow_node(
764    node_id: &str,
765    predecessor_ids: &[String],
766    expression_targets: &BTreeMap<String, String>,
767) -> Vec<String> {
768    let mut targets = BTreeSet::new();
769    if let Some(target) = expression_targets.get(node_id) {
770        targets.insert(target.clone());
771    }
772    for predecessor_id in predecessor_ids {
773        if let Some(target) = expression_targets.get(predecessor_id) {
774            targets.insert(target.clone());
775        }
776    }
777    targets.into_iter().collect()
778}
779
780fn selector_universe_for_targets(
781    target_style_paths: Option<&[String]>,
782    style_selectors_by_path: &BTreeMap<String, Vec<String>>,
783) -> Vec<String> {
784    let mut selectors = BTreeSet::new();
785    if target_style_paths.is_none_or(<[String]>::is_empty) {
786        for selector_names in style_selectors_by_path.values() {
787            selectors.extend(selector_names.iter().cloned());
788        }
789    } else if let Some(target_style_paths) = target_style_paths {
790        for target_style_path in target_style_paths {
791            if let Some(selector_names) = style_selectors_by_path.get(target_style_path) {
792                selectors.extend(selector_names.iter().cloned());
793            }
794        }
795    }
796    selectors.into_iter().collect()
797}
798
799pub fn summarize_omena_query_source_resolution_query_fragments(
800    input: &EngineInputV2,
801) -> SourceResolutionQueryFragmentsV0 {
802    summarize_omena_resolver_query_fragments(input)
803}
804
805pub fn summarize_omena_query_selector_usage_query_fragments(
806    input: &EngineInputV2,
807) -> SelectorUsageQueryFragmentsV0 {
808    summarize_selector_usage_query_fragments_input(input)
809}
810
811pub fn summarize_omena_query_source_resolution_canonical_producer_signal(
812    input: &EngineInputV2,
813) -> SourceResolutionCanonicalProducerSignalV0 {
814    summarize_omena_resolver_canonical_producer_signal(input)
815}
816
817pub fn summarize_omena_query_source_resolution_runtime(
818    input: &EngineInputV2,
819) -> OmenaResolverSourceResolutionRuntimeIndexV0 {
820    summarize_omena_resolver_source_resolution_runtime(input)
821}
822
823pub fn summarize_omena_query_expression_semantics_canonical_producer_signal(
824    input: &EngineInputV2,
825) -> ExpressionSemanticsCanonicalProducerSignalV0 {
826    summarize_expression_semantics_canonical_producer_signal_input(input)
827}
828
829pub fn summarize_omena_query_selector_usage_canonical_producer_signal(
830    input: &EngineInputV2,
831) -> SelectorUsageCanonicalProducerSignalV0 {
832    summarize_selector_usage_canonical_producer_signal_input(input)
833}
834
835#[cfg(test)]
836mod tests {
837    use super::*;
838
839    fn serialized_json_bytes<T: serde::Serialize>(
840        value: &T,
841        label: &str,
842    ) -> Result<Vec<u8>, String> {
843        serde_json::to_vec(value).map_err(|error| format!("{label} serialization failed: {error}"))
844    }
845
846    fn selector_certainty_product_input() -> EngineInputV2 {
847        let range = RangeV2 {
848            start: PositionV2 {
849                line: 0,
850                character: 0,
851            },
852            end: PositionV2 {
853                line: 0,
854                character: 11,
855            },
856        };
857        EngineInputV2 {
858            version: "selector-certainty-product".to_string(),
859            sources: vec![SourceAnalysisInputV2 {
860                document: SourceDocumentV2 {
861                    class_expressions: vec![ClassExpressionInputV2 {
862                        id: "expr-certainty".to_string(),
863                        kind: "styleAccess".to_string(),
864                        scss_module_path: "/tmp/App.module.scss".to_string(),
865                        range: range.clone(),
866                        class_name: Some("x".to_string()),
867                        root_binding_decl_id: None,
868                        access_path: Some(vec!["styles".to_string(), "x".to_string()]),
869                    }],
870                },
871            }],
872            styles: vec![StyleAnalysisInputV2 {
873                file_path: "/tmp/App.module.scss".to_string(),
874                source: None,
875                document: StyleDocumentV2 {
876                    selectors: vec![StyleSelectorV2 {
877                        name: "x".to_string(),
878                        view_kind: "canonical".to_string(),
879                        canonical_name: Some("x".to_string()),
880                        range,
881                        nested_safety: Some("safe".to_string()),
882                        composes: None,
883                        bem_suffix: None,
884                    }],
885                },
886            }],
887            type_facts: vec![TypeFactEntryV2 {
888                file_path: "/tmp/App.tsx".to_string(),
889                expression_id: "expr-certainty".to_string(),
890                facts: StringTypeFactsV2 {
891                    kind: "exact".to_string(),
892                    constraint_kind: None,
893                    values: Some(vec!["x".to_string()]),
894                    prefix: None,
895                    suffix: None,
896                    min_len: None,
897                    max_len: None,
898                    char_must: None,
899                    char_may: None,
900                    may_include_other_chars: None,
901                    provenance: None,
902                },
903                control_flow_graph: Some(engine_input_producers::TypeFactControlFlowGraphV2 {
904                    entry_block_id: "seed".to_string(),
905                    blocks: vec![
906                        engine_input_producers::TypeFactControlFlowBlockV2 {
907                            id: "seed".to_string(),
908                            kind: "assignment".to_string(),
909                            transfer_kind: "assignFacts".to_string(),
910                            successor_block_ids: vec!["loop".to_string()],
911                            symbol_ordinal: None,
912                            variable_name: None,
913                            expression_kind: None,
914                            boundary_effect: "unknownBoundary".to_string(),
915                            facts: Some(StringTypeFactsV2 {
916                                kind: "finiteSet".to_string(),
917                                constraint_kind: None,
918                                values: Some(vec!["a".to_string(), "b".to_string()]),
919                                prefix: None,
920                                suffix: None,
921                                min_len: None,
922                                max_len: None,
923                                char_must: None,
924                                char_may: None,
925                                may_include_other_chars: None,
926                                provenance: None,
927                            }),
928                        },
929                        engine_input_producers::TypeFactControlFlowBlockV2 {
930                            id: "loop".to_string(),
931                            kind: "loop".to_string(),
932                            transfer_kind: "concatFacts".to_string(),
933                            successor_block_ids: vec!["loop".to_string()],
934                            symbol_ordinal: None,
935                            variable_name: None,
936                            expression_kind: None,
937                            boundary_effect: "unknownBoundary".to_string(),
938                            facts: None,
939                        },
940                    ],
941                }),
942            }],
943        }
944    }
945
946    fn selector_certainty_colon_collision_input() -> EngineInputV2 {
947        let mut input = selector_certainty_product_input();
948        input.sources[0].document.class_expressions[0].id = "b:c".to_string();
949        input.sources[0].document.class_expressions[0].class_name = Some("x".to_string());
950        input.sources[0].document.class_expressions[0].access_path =
951            Some(vec!["styles".to_string(), "x".to_string()]);
952        let second_expression = ClassExpressionInputV2 {
953            id: "c".to_string(),
954            kind: "styleAccess".to_string(),
955            scss_module_path: "/tmp/App.module.scss".to_string(),
956            range: input.sources[0].document.class_expressions[0].range.clone(),
957            class_name: Some("y".to_string()),
958            root_binding_decl_id: None,
959            access_path: Some(vec!["styles".to_string(), "y".to_string()]),
960        };
961        input.sources[0]
962            .document
963            .class_expressions
964            .push(second_expression);
965
966        let second_selector = StyleSelectorV2 {
967            name: "y".to_string(),
968            view_kind: "canonical".to_string(),
969            canonical_name: Some("y".to_string()),
970            range: input.styles[0].document.selectors[0].range.clone(),
971            nested_safety: Some("safe".to_string()),
972            composes: None,
973            bem_suffix: None,
974        };
975        input.styles[0].document.selectors.push(second_selector);
976
977        let mut first_fact = input.type_facts[0].clone();
978        first_fact.file_path = "/tmp/A".to_string();
979        first_fact.expression_id = "b:c".to_string();
980        let mut second_fact = first_fact.clone();
981        second_fact.file_path = "/tmp/A:b".to_string();
982        second_fact.expression_id = "c".to_string();
983        second_fact.facts.values = Some(vec!["y".to_string()]);
984        if let Some(second_graph) = second_fact.control_flow_graph.as_mut() {
985            second_graph.blocks.truncate(1);
986            second_graph.blocks[0].successor_block_ids.clear();
987        }
988        input.type_facts = vec![first_fact, second_fact];
989        input
990    }
991
992    #[test]
993    fn expression_domain_runtime_reuses_graph_databases_across_revisions() {
994        let input = EngineInputV2 {
995            version: "core-runtime".to_string(),
996            sources: Vec::new(),
997            styles: Vec::new(),
998            type_facts: Vec::new(),
999        };
1000        let mut runtime = OmenaQueryExpressionDomainFlowRuntimeV0::default();
1001
1002        let first =
1003            summarize_omena_query_expression_domain_incremental_flow_analysis(&input, &mut runtime);
1004        let second =
1005            summarize_omena_query_expression_domain_incremental_flow_analysis(&input, &mut runtime);
1006
1007        assert_eq!(first.revision, 1);
1008        assert_eq!(second.revision, 2);
1009        assert_eq!(runtime.revision(), 2);
1010    }
1011
1012    #[test]
1013    fn expression_domain_runtime_checks_sealed_artifacts_before_product_reuse() {
1014        let input = selector_certainty_product_input();
1015        let mut runtime = OmenaQueryExpressionDomainFlowRuntimeV0::default();
1016
1017        let first =
1018            summarize_omena_query_expression_domain_incremental_flow_analysis(&input, &mut runtime);
1019        assert!(
1020            first.graph_count > 0,
1021            "fixture must exercise product flow graphs"
1022        );
1023        assert_eq!(runtime.read_set_digest_check_count(), 0);
1024        assert_eq!(runtime.analysis_rebuild_count(), first.graph_count);
1025
1026        let second =
1027            summarize_omena_query_expression_domain_incremental_flow_analysis(&input, &mut runtime);
1028        eprintln!(
1029            "productFlowArtifactCounters graphs={} digestChecks={} rebuilds={} refusals={}",
1030            second.graph_count,
1031            runtime.read_set_digest_check_count(),
1032            runtime.analysis_rebuild_count(),
1033            runtime.artifact_refusal_count(),
1034        );
1035        assert_eq!(second.reused_graph_count, second.graph_count);
1036        assert_eq!(runtime.read_set_digest_check_count(), second.graph_count);
1037        assert_eq!(
1038            runtime.analysis_rebuild_count(),
1039            first.graph_count,
1040            "verified reuse must not rebuild the flow analysis"
1041        );
1042        assert_eq!(runtime.artifact_refusal_count(), 0);
1043    }
1044
1045    #[test]
1046    fn expression_domain_runtime_preserves_legacy_incremental_plan_bytes_across_reuse_revisions()
1047    -> Result<(), String> {
1048        let input = selector_certainty_product_input();
1049        let flow_graphs = collect_expression_domain_flow_graphs(&input);
1050        assert_eq!(
1051            flow_graphs.len(),
1052            1,
1053            "fixture must isolate one product graph"
1054        );
1055        let graph = &flow_graphs[0].graph;
1056        let mut runtime = OmenaQueryExpressionDomainFlowRuntimeV0::default();
1057        let mut legacy_snapshot = None;
1058        let mut legacy_analysis = None;
1059
1060        for revision in 1..=5 {
1061            let product = summarize_omena_query_expression_domain_incremental_flow_analysis(
1062                &input,
1063                &mut runtime,
1064            );
1065            let legacy = omena_abstract_value::analyze_class_value_flow_incremental_with_reuse(
1066                graph,
1067                legacy_snapshot.as_ref(),
1068                legacy_analysis.as_ref(),
1069                revision,
1070            );
1071            let product_analysis = &product.analyses[0].analysis;
1072            let product_plan_bytes = serialized_json_bytes(
1073                &product_analysis.incremental_plan,
1074                "product incremental plan",
1075            )?;
1076            let legacy_plan_bytes =
1077                serialized_json_bytes(&legacy.incremental_plan, "legacy incremental plan")?;
1078            let product_changed_at = product_analysis
1079                .incremental_plan
1080                .nodes
1081                .iter()
1082                .map(|node| node.changed_at.value)
1083                .collect::<Vec<_>>();
1084            let legacy_changed_at = legacy
1085                .incremental_plan
1086                .nodes
1087                .iter()
1088                .map(|node| node.changed_at.value)
1089                .collect::<Vec<_>>();
1090            assert_eq!(
1091                product_changed_at, legacy_changed_at,
1092                "unchanged product nodes must preserve the legacy changedAt stamps at revision {revision}"
1093            );
1094            assert_eq!(
1095                product_plan_bytes, legacy_plan_bytes,
1096                "incrementalPlan bytes diverged from the independent legacy oracle at revision {revision}"
1097            );
1098            assert_eq!(
1099                serialized_json_bytes(product_analysis, "product incremental analysis")?,
1100                serialized_json_bytes(&legacy, "legacy incremental analysis")?,
1101                "public incremental analysis bytes diverged at revision {revision}"
1102            );
1103            if revision > 1 {
1104                assert_eq!(product.reused_graph_count, 1);
1105                assert!(product_analysis.reused_previous_analysis);
1106                assert!(legacy.reused_previous_analysis);
1107            }
1108            legacy_snapshot = Some(legacy.next_snapshot);
1109            legacy_analysis = Some(legacy.analysis);
1110        }
1111
1112        let mut changed_input = selector_certainty_product_input();
1113        changed_input.type_facts[0].facts.values = Some(vec!["changed".to_string()]);
1114        let changed_graphs = collect_expression_domain_flow_graphs(&changed_input);
1115        let changed_product = summarize_omena_query_expression_domain_incremental_flow_analysis(
1116            &changed_input,
1117            &mut runtime,
1118        );
1119        let changed_legacy = omena_abstract_value::analyze_class_value_flow_incremental_with_reuse(
1120            &changed_graphs[0].graph,
1121            legacy_snapshot.as_ref(),
1122            legacy_analysis.as_ref(),
1123            6,
1124        );
1125        assert_eq!(changed_product.reused_graph_count, 0);
1126        assert!(
1127            !changed_product.analyses[0]
1128                .analysis
1129                .reused_previous_analysis
1130        );
1131        assert!(!changed_legacy.reused_previous_analysis);
1132        assert_eq!(
1133            serialized_json_bytes(
1134                &changed_product.analyses[0].analysis,
1135                "changed product analysis",
1136            )?,
1137            serialized_json_bytes(&changed_legacy, "changed legacy analysis")?,
1138            "a member change after consecutive reuse revisions must match the independent legacy oracle"
1139        );
1140
1141        eprintln!(
1142            "productFlowArtifactByteIdentity graphs=1 reuseRevisions=4 memberChanges=1 digestChecks={} rebuilds={} refusals={}",
1143            runtime.read_set_digest_check_count(),
1144            runtime.analysis_rebuild_count(),
1145            runtime.artifact_refusal_count(),
1146        );
1147        assert_eq!(runtime.read_set_digest_check_count(), 5);
1148        assert_eq!(runtime.analysis_rebuild_count(), 2);
1149        assert_eq!(runtime.artifact_refusal_count(), 0);
1150        Ok(())
1151    }
1152
1153    #[test]
1154    fn expression_domain_runtime_refusal_fallback_rebuilds_and_recovers() -> Result<(), String> {
1155        let input = selector_certainty_product_input();
1156        let graph = collect_expression_domain_flow_graphs(&input)
1157            .pop()
1158            .ok_or_else(|| "fixture must produce one graph".to_string())?
1159            .graph;
1160        let mut runtime = OmenaQueryExpressionDomainFlowRuntimeV0::default();
1161
1162        let first = runtime.analyze_input(&input);
1163        let second = runtime.analyze_input(&input);
1164        assert_eq!(first.graph_count, 1);
1165        assert_eq!(second.reused_graph_count, 1);
1166
1167        let mut injected = false;
1168        let third = runtime.analyze_input_with_artifact_analyzer(
1169            &input,
1170            |graph, previous_artifact, revision| {
1171                if revision == 3 && previous_artifact.is_some() && !injected {
1172                    injected = true;
1173                    return Err(omena_abstract_value::ClassValueFlowArtifactRefusalV0 {
1174                        schema_version: "0",
1175                        product: "omena-abstract-value.flow-analysis-artifact-refusal",
1176                        cause: omena_abstract_value::ClassValueFlowArtifactRefusalCauseV0::ArtifactDigestMismatch,
1177                    });
1178                }
1179                analyze_class_value_flow_incremental_with_artifact(
1180                    graph,
1181                    previous_artifact,
1182                    revision,
1183                )
1184            },
1185        );
1186        assert!(injected, "the product refusal seam must be exercised");
1187        assert_eq!(third.reused_graph_count, 0);
1188        assert_eq!(runtime.artifact_refusal_count(), 1);
1189        assert_eq!(runtime.analysis_rebuild_count(), 2);
1190        assert_eq!(
1191            serialized_json_bytes(&third.analyses[0].analysis, "fallback analysis")?,
1192            serialized_json_bytes(
1193                &analyze_class_value_flow_incremental(&graph, None, 3),
1194                "fresh fallback oracle",
1195            )?,
1196            "a refused artifact must serve the fresh fallback value"
1197        );
1198
1199        let fourth = runtime.analyze_input(&input);
1200        assert_eq!(fourth.reused_graph_count, 0);
1201        assert_eq!(runtime.analysis_rebuild_count(), 3);
1202        assert_eq!(runtime.artifact_refusal_count(), 1);
1203        let fifth = runtime.analyze_input(&input);
1204        assert_eq!(fifth.reused_graph_count, 1);
1205        assert_eq!(runtime.analysis_rebuild_count(), 3);
1206        assert_eq!(runtime.artifact_refusal_count(), 1);
1207        assert_eq!(
1208            fifth.analyses[0].analysis.analysis,
1209            fourth.analyses[0].analysis.analysis
1210        );
1211        eprintln!(
1212            "productFlowArtifactRefusalRecovery graphs=1 refusalRevision=3 fallbackRebuilds=1 reseedRebuilds=1 reuseResumedRevision=5 totalRebuilds={} refusals={}",
1213            runtime.analysis_rebuild_count(),
1214            runtime.artifact_refusal_count(),
1215        );
1216        Ok(())
1217    }
1218
1219    #[test]
1220    fn nonconverged_flow_hedge_demotes_typed_query_projection() {
1221        let input = selector_certainty_product_input();
1222        let graph_id = "/tmp/App.tsx:expr-certainty:expression-domain-control-flow";
1223        let control_flow = summarize_omena_query_expression_domain_control_flow_analysis(&input);
1224        let projection = summarize_omena_query_expression_domain_selector_projection(&input);
1225        let control_entry = &control_flow.analyses[0];
1226        let entry = &projection.projections[0];
1227
1228        println!(
1229            "certainty-hedge-census graphId={graph_id} hedged=1 base=exact certainty=possible"
1230        );
1231        assert_eq!(control_entry.graph_id, graph_id);
1232        assert!(!control_entry.analysis.flow_analysis.converged);
1233        assert!(
1234            control_entry
1235                .analysis
1236                .flow_analysis
1237                .nodes
1238                .iter()
1239                .all(|node| {
1240                    matches!(
1241                        &node.value,
1242                        AbstractClassValueV0::Top {
1243                            provenance: Some(AbstractClassValueProvenanceV0::FlowIterationLimit)
1244                        }
1245                    )
1246                })
1247        );
1248        assert_eq!(entry.value_kind, "exact");
1249        assert_eq!(entry.selector_names, vec!["x".to_string()]);
1250        assert_eq!(entry.certainty, SelectorProjectionCertaintyV0::Possible);
1251    }
1252
1253    #[test]
1254    fn colon_colliding_graph_ids_bind_selector_certainty_by_type_fact_tuple() {
1255        let input = selector_certainty_colon_collision_input();
1256        let control_flow = summarize_omena_query_expression_domain_control_flow_analysis(&input);
1257        let projection = summarize_omena_query_expression_domain_selector_projection(&input);
1258
1259        assert_eq!(control_flow.analyses.len(), 2);
1260        assert_eq!(
1261            control_flow.analyses[0].graph_id, control_flow.analyses[1].graph_id,
1262            "fixture must retain the diagnostic graph-id collision"
1263        );
1264        let nonconverged = projection
1265            .projections
1266            .iter()
1267            .filter(|entry| entry.file_path == "/tmp/A" && entry.node_id == "b:c")
1268            .collect::<Vec<_>>();
1269        let converged = projection
1270            .projections
1271            .iter()
1272            .filter(|entry| entry.file_path == "/tmp/A:b" && entry.node_id == "c")
1273            .collect::<Vec<_>>();
1274
1275        assert_eq!(nonconverged.len(), 1);
1276        assert_eq!(converged.len(), 1);
1277        assert_eq!(
1278            nonconverged[0].certainty,
1279            SelectorProjectionCertaintyV0::Possible
1280        );
1281        assert_eq!(converged[0].certainty, SelectorProjectionCertaintyV0::Exact);
1282    }
1283
1284    #[test]
1285    #[should_panic(expected = "selector-certainty flow hedge duplicate type-fact key")]
1286    fn duplicate_selector_certainty_type_fact_key_fails_closed() {
1287        let mut input = selector_certainty_product_input();
1288        input.type_facts.push(input.type_facts[0].clone());
1289
1290        let _ = summarize_omena_query_expression_domain_selector_projection(&input);
1291    }
1292
1293    #[test]
1294    #[should_panic(
1295        expected = "selector-certainty flow hedge requires one control analysis per control-flow type fact"
1296    )]
1297    fn missing_selector_certainty_control_analysis_fails_closed() {
1298        let input = selector_certainty_product_input();
1299        let mut control_flow =
1300            summarize_omena_query_expression_domain_control_flow_analysis(&input);
1301        control_flow.analyses.clear();
1302
1303        let _ = bind_expression_domain_selector_certainty_flow_hedges(&input, control_flow);
1304    }
1305
1306    #[test]
1307    fn analysis_precision_view_maps_known_producers_and_fails_closed() {
1308        let precision = |value_domain: &str| OmenaQueryAnalysisPrecisionV0 {
1309            product: "omena-query.analysis-precision".to_string(),
1310            value_domain: value_domain.to_string(),
1311            flow_sensitivity: "fixture".to_string(),
1312            context_sensitivity: "fixture".to_string(),
1313            revision_axis: "fixture".to_string(),
1314        };
1315
1316        assert_eq!(
1317            fact_precision_from_analysis_precision(&precision("cascadeAtPosition")),
1318            FactPrecision::Exact
1319        );
1320        assert_eq!(
1321            fact_precision_from_analysis_precision(&precision("styleModuleResolution")),
1322            FactPrecision::Exact
1323        );
1324        assert_eq!(
1325            fact_precision_from_analysis_precision(&precision("classValueResolution")),
1326            FactPrecision::Conservative
1327        );
1328        assert_eq!(
1329            fact_precision_from_analysis_precision(&precision("classValueUniverse")),
1330            FactPrecision::Conservative
1331        );
1332        assert_eq!(
1333            fact_precision_from_analysis_precision(&precision("classValueFlow")),
1334            FactPrecision::Heuristic
1335        );
1336        assert_eq!(
1337            fact_precision_from_analysis_precision(&precision("unknown")),
1338            FactPrecision::Unknown
1339        );
1340        assert_eq!(
1341            fact_precision_from_analysis_precision(&precision("unregistered")),
1342            FactPrecision::Unknown
1343        );
1344    }
1345}