Skip to main content

omena_query/
style.rs

1use super::*;
2use omena_parser::{ParsedSassIncludeFact, ParsedSelectorFact, ParsedVariableFact};
3use omena_syntax::css_keyword;
4use std::cell::RefCell;
5use std::path::{Path, PathBuf};
6
7mod cascade_position;
8mod code_actions;
9mod completion;
10mod cross_file_hypergraph;
11mod cross_file_summary;
12mod diagnostic_suppressions;
13mod diagnostics;
14mod dynamic_classname;
15mod insights;
16mod module_interface;
17mod origin_inputs;
18mod parser_facade;
19mod registered_property_values;
20#[cfg(feature = "salsa-memo")]
21mod salsa_memo;
22mod sass;
23mod source_refs;
24mod stylesheet_evaluation;
25mod substrate;
26mod transform;
27
28#[cfg(test)]
29pub(crate) use cascade_checker::cascade_declarations_collect_probe;
30pub use cascade_position::*;
31pub use code_actions::*;
32pub use completion::*;
33#[cfg(feature = "hypergraph-ifds")]
34pub use cross_file_hypergraph::*;
35use cross_file_summary::summarize_omena_query_cross_file_summary;
36#[cfg(any(test, feature = "test-support"))]
37pub use cross_file_summary::{
38    read_workspace_cross_file_summary_direct_recompute_count_for_test,
39    read_workspace_cross_file_summary_internal_compute_count_for_test,
40    reset_workspace_cross_file_summary_direct_recompute_count_for_test,
41    reset_workspace_cross_file_summary_internal_compute_count_for_test,
42};
43pub use cross_file_summary::{
44    summarize_omena_query_categorical_design_system_cross_project_summary,
45    summarize_omena_query_m4_axis_c_readiness,
46    summarize_omena_query_source_selector_reference_cross_file_summary,
47    summarize_omena_query_source_selector_reference_cross_file_summary_with_resolution_inputs,
48    summarize_omena_query_workspace_cross_file_summary,
49    summarize_omena_query_workspace_cross_file_summary_with_resolution_inputs,
50};
51#[cfg(test)]
52pub(crate) use diagnostics::collect_omena_query_visible_sass_symbol_keys_for_workspace_file;
53pub use diagnostics::*;
54pub use dynamic_classname::*;
55pub use insights::*;
56use module_interface::summarize_css_modules_interface_bundle_from_projections;
57pub use module_interface::{
58    OmenaQueryCssModuleClassExportV0, OmenaQueryCssModuleClassReferenceV0,
59    OmenaQueryCssModuleIcssExportV0, OmenaQueryCssModuleInterfaceV0,
60    OmenaQueryCssModulesInterfaceBundleV0, OmenaQueryCssModulesInterfaceSummaryViewV0,
61    render_omena_query_css_module_typescript_declaration,
62    render_omena_query_css_modules_interface_json,
63    summarize_omena_query_css_modules_interface_summary_view,
64};
65pub use origin_inputs::*;
66#[cfg(test)]
67pub(crate) use parser_facade::style_facts_collect_probe;
68pub use parser_facade::{
69    OmenaQueryStyleFrameRefreshFactsV0, OmenaQueryStyleFrameRefreshParseCacheV0,
70    summarize_omena_query_omena_parser_css_modules_intermediate,
71    summarize_omena_query_omena_parser_lex, summarize_omena_query_omena_parser_style_facts,
72    summarize_omena_query_sass_module_source_edges, summarize_omena_query_style_document,
73    summarize_omena_query_style_frame_refresh_facts_with_reuse,
74};
75use parser_facade::{
76    collect_omena_query_omena_parser_style_facts_raw,
77    collect_omena_query_style_facts_with_icss_values_raw, omena_parser_dialect_for_style_path,
78    omena_parser_style_dialect_label, omena_query_sass_symbol_fact_kind_is_declaration,
79    omena_query_sass_symbol_fact_kind_is_reference,
80    summarize_omena_query_omena_parser_style_facts_from_facts,
81};
82pub use registered_property_values::*;
83#[cfg(feature = "salsa-memo")]
84pub use salsa_memo::*;
85pub use sass::*;
86pub use source_refs::*;
87pub use substrate::*;
88pub use transform::*;
89
90mod cascade_checker;
91
92#[derive(Debug, Clone, PartialEq, Eq, Serialize)]
93#[serde(rename_all = "camelCase")]
94pub struct OmenaQueryCascadeSiteOutcomeV0 {
95    pub schema_version: &'static str,
96    pub product: &'static str,
97    pub selector: String,
98    pub property: String,
99    pub winning_value: String,
100}
101
102/// Project the product cascade scanner and ranker onto stable site outcomes.
103///
104/// The projection intentionally excludes custom properties because their
105/// computed value depends on the workspace fixed point rather than one file.
106pub fn summarize_omena_query_cascade_site_outcomes_from_source(
107    source: &str,
108) -> Vec<OmenaQueryCascadeSiteOutcomeV0> {
109    let mut outcomes = cascade_checker::collect_query_replica_ensemble_site_outcomes(source)
110        .into_iter()
111        .filter_map(|site_outcome| {
112            let omena_cascade::CascadeOutcome::Definite { winner, .. } = site_outcome.outcome
113            else {
114                return None;
115            };
116            let winning_value = match winner.value {
117                omena_cascade::CascadeValue::Literal(value) => value,
118                _ => winner.id,
119            };
120            Some(OmenaQueryCascadeSiteOutcomeV0 {
121                schema_version: "0",
122                product: "omena-query.cascade-site-outcome",
123                selector: site_outcome.site.element_selector,
124                property: site_outcome.site.property,
125                winning_value,
126            })
127        })
128        .collect::<Vec<_>>();
129    outcomes.sort_by(|left, right| {
130        left.selector
131            .cmp(&right.selector)
132            .then_with(|| left.property.cmp(&right.property))
133            .then_with(|| left.winning_value.cmp(&right.winning_value))
134    });
135    outcomes
136}
137
138#[cfg(test)]
139mod cascade_site_outcome_tests {
140    use super::*;
141
142    #[test]
143    fn cascade_site_projection_uses_the_product_source_order_winner() {
144        let outcomes = summarize_omena_query_cascade_site_outcomes_from_source(
145            ".card { color: red; } .card { color: blue; }",
146        );
147
148        assert_eq!(outcomes.len(), 1);
149        assert_eq!(outcomes[0].selector, ".card");
150        assert_eq!(outcomes[0].property, "color");
151        assert_eq!(outcomes[0].winning_value, "blue");
152    }
153}
154
155pub fn summarize_omena_query_style_semantic_graph_from_source(
156    style_path: &str,
157    style_source: &str,
158    input: &EngineInputV2,
159) -> Option<StyleSemanticGraphSummaryV0> {
160    summarize_omena_bridge_style_semantic_graph_from_source(style_path, style_source, input)
161}
162
163pub fn read_omena_query_style_context_index(
164    style_path: &str,
165    style_source: &str,
166    input: &EngineInputV2,
167) -> Option<OmenaQueryStyleContextIndexV0> {
168    let graph =
169        summarize_omena_query_style_semantic_graph_from_source(style_path, style_source, input)?;
170    Some(OmenaQueryStyleContextIndexV0 {
171        schema_version: "0",
172        product: "omena-query.style-context-index",
173        style_path: style_path.to_string(),
174        language: graph.language,
175        context_index_source: graph.semantic_facts.context_index.product,
176        context_index: graph.semantic_facts.context_index,
177    })
178}
179
180pub fn summarize_omena_query_style_hover_candidates(
181    style_path: &str,
182    style_source: &str,
183) -> Option<OmenaQueryStyleHoverCandidatesV0> {
184    let dialect = omena_parser_dialect_for_style_path(style_path);
185    let facts = collect_omena_query_omena_parser_style_facts_raw(style_source, dialect);
186    let mut seen = BTreeSet::new();
187    let mut candidates = Vec::new();
188    collect_style_selector_hover_candidates_from_omena_parser_facts(
189        style_source,
190        facts.selectors.as_slice(),
191        &mut seen,
192        &mut candidates,
193    );
194    collect_custom_property_hover_candidates_from_omena_parser_facts(
195        style_source,
196        facts.variables.as_slice(),
197        &mut seen,
198        &mut candidates,
199    );
200    collect_sass_symbol_hover_candidates_from_omena_parser_facts(
201        style_source,
202        facts.sass_symbols.as_slice(),
203        &mut seen,
204        &mut candidates,
205    );
206    collect_sass_partial_evaluator_selector_candidates_from_omena_parser_facts(
207        style_source,
208        facts.sass_includes.as_slice(),
209        &mut seen,
210        &mut candidates,
211    );
212    candidates.sort();
213    Some(OmenaQueryStyleHoverCandidatesV0 {
214        schema_version: "0",
215        product: "omena-query.style-hover-candidates",
216        language: omena_parser_style_dialect_label(dialect),
217        candidates,
218    })
219}
220
221pub fn summarize_omena_query_custom_property_occurrence_index(
222    style_sources: &[OmenaQueryStyleSourceInputV0],
223) -> OmenaQueryCustomPropertyOccurrenceIndexV0 {
224    let mut occurrences = Vec::new();
225    for style in style_sources {
226        let dialect = omena_parser_dialect_for_style_path(style.style_path.as_str());
227        let facts =
228            collect_omena_query_omena_parser_style_facts_raw(style.style_source.as_str(), dialect);
229        for fact in facts.variables {
230            let kind = match fact.kind {
231                ParsedVariableFactKind::CustomPropertyDeclaration => "customPropertyDeclaration",
232                ParsedVariableFactKind::CustomPropertyReference => "customPropertyReference",
233                _ => continue,
234            };
235            let byte_span = ParserByteSpanV0 {
236                start: u32::from(fact.range.start()) as usize,
237                end: u32::from(fact.range.end()) as usize,
238            };
239            occurrences.push(OmenaQueryCustomPropertyOccurrenceV0 {
240                uri: style.style_path.clone(),
241                name: fact.name,
242                range: parser_range_for_byte_span(style.style_source.as_str(), byte_span),
243                byte_span,
244                kind,
245                has_fallback: fact.has_fallback,
246                source: "omenaParserVariableFacts",
247            });
248        }
249    }
250    occurrences.sort();
251    occurrences.dedup();
252    OmenaQueryCustomPropertyOccurrenceIndexV0 {
253        schema_version: "0",
254        product: "omena-query.custom-property-occurrence-index",
255        occurrence_count: occurrences.len(),
256        occurrences,
257        ready_surfaces: vec!["customPropertyOccurrenceIndex", "customPropertyMigration"],
258    }
259}
260
261pub fn summarize_omena_query_style_hover_render_parts(
262    source: &str,
263    kind: &str,
264    name: &str,
265    position: ParserPositionV0,
266) -> OmenaQueryStyleHoverRenderPartsV0 {
267    summarize_omena_query_style_hover_render_parts_with_branch_scope(
268        source, kind, name, position, None, None,
269    )
270}
271
272pub fn summarize_omena_query_style_hover_render_parts_for_hover_position(
273    source: &str,
274    kind: &str,
275    name: &str,
276    position: ParserPositionV0,
277) -> OmenaQueryStyleHoverRenderPartsV0 {
278    let branch_scope = (kind == "selector")
279        .then(|| selector_hover_branch_scope_at_position(source, name, position))
280        .flatten();
281    summarize_omena_query_style_hover_render_parts_with_branch_scope(
282        source,
283        kind,
284        name,
285        position,
286        branch_scope,
287        None,
288    )
289}
290
291fn summarize_omena_query_style_hover_render_parts_with_branch_scope(
292    source: &str,
293    kind: &str,
294    name: &str,
295    position: ParserPositionV0,
296    selector_branch_scope: Option<HoverCascadeBranchScope>,
297    precollected_target_declarations: Option<&[cascade_checker::QueryCheckerCascadeDeclaration]>,
298) -> OmenaQueryStyleHoverRenderPartsV0 {
299    let mut parts = OmenaQueryStyleHoverRenderPartsV0 {
300        schema_version: "0",
301        product: "omena-query.style-hover-render-parts",
302        snippet: String::new(),
303        value: None,
304        signature: None,
305        property_value_narrowings: Vec::new(),
306        render_source: "lineSnippet",
307    };
308
309    match kind {
310        "selector" => {
311            parts.snippet = rule_snippet_around_position(source, position).unwrap_or_else(|| {
312                parts.render_source = "selectorFallback";
313                format!(".{name} {{ ... }}")
314            });
315            if parts.render_source != "selectorFallback" {
316                parts.render_source = "ruleSnippet";
317            }
318            parts.property_value_narrowings = match precollected_target_declarations {
319                Some(declarations) => selector_property_value_narrowings_from_declarations(
320                    declarations,
321                    name,
322                    selector_branch_scope.as_ref(),
323                ),
324                None => selector_property_value_narrowings_for_hover(
325                    source,
326                    name,
327                    selector_branch_scope.as_ref(),
328                ),
329            };
330        }
331        "customPropertyReference" | "customPropertyDeclaration" => {
332            parts.snippet = line_snippet_at_position(source, position).unwrap_or_default();
333        }
334        kind if is_sass_symbol_candidate_kind(kind) => {
335            parts.snippet = line_snippet_at_position(source, position).unwrap_or_default();
336            if sass_symbol_kind_from_candidate_kind(kind) == Some("variable")
337                && is_sass_symbol_declaration_kind(kind)
338            {
339                parts.value = sass_variable_value_from_declaration_line(parts.snippet.as_str());
340            } else if matches!(
341                sass_symbol_kind_from_candidate_kind(kind),
342                Some("mixin" | "function")
343            ) && is_sass_symbol_declaration_kind(kind)
344                && let Some((signature, snippet)) =
345                    sass_callable_definition_render_parts(source, position)
346            {
347                parts.signature = Some(signature);
348                parts.snippet = snippet;
349                parts.render_source = "callableBlockSnippet";
350            }
351        }
352        _ => {
353            parts.snippet = name.to_string();
354            parts.render_source = "candidateNameFallback";
355        }
356    }
357
358    parts
359}
360
361pub fn summarize_omena_query_style_hover_render_parts_for_workspace_file(
362    target_style_path: &str,
363    style_sources: &[OmenaQueryStyleSourceInputV0],
364    package_manifests: &[OmenaQueryStylePackageManifestV0],
365    resolution_inputs: &OmenaQueryStyleResolutionInputsV0,
366    kind: &str,
367    name: &str,
368    position: ParserPositionV0,
369) -> Option<OmenaQueryStyleHoverRenderPartsV0> {
370    let target = style_sources
371        .iter()
372        .find(|source| source.style_path == target_style_path)?;
373    let mut parts =
374        summarize_omena_query_style_hover_render_parts(&target.style_source, kind, name, position);
375    if kind == "selector" {
376        let module_graph_narrowings = selector_property_value_narrowings_for_hover_module_graph(
377            target_style_path,
378            style_sources,
379            package_manifests,
380            resolution_inputs.bundler_path_mappings.as_slice(),
381            resolution_inputs.tsconfig_path_mappings.as_slice(),
382            name,
383            None,
384        );
385        if !module_graph_narrowings.is_empty() {
386            parts.property_value_narrowings = module_graph_narrowings;
387        }
388    }
389    Some(parts)
390}
391
392pub fn summarize_omena_query_style_hover_render_parts_for_workspace_file_hover_position(
393    target_style_path: &str,
394    style_sources: &[OmenaQueryStyleSourceInputV0],
395    package_manifests: &[OmenaQueryStylePackageManifestV0],
396    resolution_inputs: &OmenaQueryStyleResolutionInputsV0,
397    kind: &str,
398    name: &str,
399    position: ParserPositionV0,
400) -> Option<OmenaQueryStyleHoverRenderPartsV0> {
401    let target = style_sources
402        .iter()
403        .find(|source| source.style_path == target_style_path)?;
404    let branch_scope = (kind == "selector")
405        .then(|| selector_hover_branch_scope_at_position(&target.style_source, name, position))
406        .flatten();
407    let mut parts = summarize_omena_query_style_hover_render_parts_with_branch_scope(
408        &target.style_source,
409        kind,
410        name,
411        position,
412        branch_scope.clone(),
413        None,
414    );
415    if kind == "selector" {
416        let module_graph_narrowings = selector_property_value_narrowings_for_hover_module_graph(
417            target_style_path,
418            style_sources,
419            package_manifests,
420            resolution_inputs.bundler_path_mappings.as_slice(),
421            resolution_inputs.tsconfig_path_mappings.as_slice(),
422            name,
423            branch_scope.as_ref(),
424        );
425        if !module_graph_narrowings.is_empty() {
426            parts.property_value_narrowings = module_graph_narrowings;
427        }
428    }
429    Some(parts)
430}
431
432/// Substrate-backed variant of
433/// [`summarize_omena_query_style_hover_render_parts_for_workspace_file`]: the
434/// name-independent collection (per-file cascade declarations + cross-file resolution)
435/// comes precollected, so only the per-name narrowing runs here. The substrate MUST
436/// have been built from the same `style_sources` (rfcs#63 E-ii).
437pub fn summarize_omena_query_style_hover_render_parts_for_workspace_file_with_substrate(
438    target_style_path: &str,
439    style_sources: &[OmenaQueryStyleSourceInputV0],
440    substrate: &OmenaQueryStyleCascadeNarrowingSubstrateV0,
441    kind: &str,
442    name: &str,
443    position: ParserPositionV0,
444) -> Option<OmenaQueryStyleHoverRenderPartsV0> {
445    let target = style_sources
446        .iter()
447        .find(|source| source.style_path == target_style_path)?;
448    summarize_omena_query_style_hover_render_parts_for_target_with_substrate(
449        target_style_path,
450        &target.style_source,
451        substrate,
452        kind,
453        name,
454        position,
455        // Mirror the non-substrate workspace-file variant: no hovered-branch narrowing.
456        None,
457    )
458}
459
460/// Substrate-backed variant of
461/// [`summarize_omena_query_style_hover_render_parts_for_workspace_file_hover_position`].
462pub fn summarize_omena_query_style_hover_render_parts_for_workspace_file_hover_position_with_substrate(
463    target_style_path: &str,
464    style_sources: &[OmenaQueryStyleSourceInputV0],
465    substrate: &OmenaQueryStyleCascadeNarrowingSubstrateV0,
466    kind: &str,
467    name: &str,
468    position: ParserPositionV0,
469) -> Option<OmenaQueryStyleHoverRenderPartsV0> {
470    let target = style_sources
471        .iter()
472        .find(|source| source.style_path == target_style_path)?;
473    let branch_scope = (kind == "selector")
474        .then(|| selector_hover_branch_scope_at_position(&target.style_source, name, position))
475        .flatten();
476    summarize_omena_query_style_hover_render_parts_for_target_with_substrate(
477        target_style_path,
478        &target.style_source,
479        substrate,
480        kind,
481        name,
482        position,
483        branch_scope,
484    )
485}
486
487fn summarize_omena_query_style_hover_render_parts_for_target_with_substrate(
488    target_style_path: &str,
489    target_style_source: &str,
490    substrate: &OmenaQueryStyleCascadeNarrowingSubstrateV0,
491    kind: &str,
492    name: &str,
493    position: ParserPositionV0,
494    branch_scope: Option<HoverCascadeBranchScope>,
495) -> Option<OmenaQueryStyleHoverRenderPartsV0> {
496    let mut parts = summarize_omena_query_style_hover_render_parts_with_branch_scope(
497        target_style_source,
498        kind,
499        name,
500        position,
501        branch_scope.clone(),
502        substrate.declarations_for_style_path(target_style_path),
503    );
504    if kind == "selector" {
505        let module_graph_narrowings =
506            selector_property_value_narrowings_for_hover_module_graph_with_substrate(
507                target_style_path,
508                substrate,
509                name,
510                branch_scope.as_ref(),
511            );
512        if !module_graph_narrowings.is_empty() {
513            parts.property_value_narrowings = module_graph_narrowings;
514        }
515    }
516    Some(parts)
517}
518
519#[derive(Clone, Debug, Eq, Ord, PartialEq, PartialOrd)]
520struct HoverCascadeBranchScope {
521    condition_context: Vec<String>,
522    layer_name: Option<String>,
523    layer_order: Option<i32>,
524}
525
526#[derive(Clone, Debug, Eq, Ord, PartialEq, PartialOrd)]
527struct HoverCascadeBranchMatch {
528    span_len: usize,
529    scope: HoverCascadeBranchScope,
530}
531
532type SelectorPropertyBranchKey = (String, Vec<String>, Option<String>, Option<i32>);
533
534/// Name-independent cascade-narrowing inputs precollected over a fixed style corpus
535/// (rfcs#63 E-ii): per-file cascade declarations in `style_sources` order plus the
536/// cross-file resolution. Building it costs one collection pass over the corpus; every
537/// subsequent per-name narrowing (hover, completion documentation) is a cheap filter.
538/// Only valid for the exact `(style_sources, package_manifests, resolution_inputs)` it
539/// was built from — callers own that cache-key discipline.
540#[derive(Debug, Clone, PartialEq, Eq)]
541pub struct OmenaQueryStyleCascadeNarrowingSubstrateV0 {
542    entries: Vec<StyleCascadeNarrowingSubstrateEntry>,
543    resolution: OmenaQuerySassModuleCrossFileResolutionV0,
544}
545
546#[derive(Debug, Clone, PartialEq, Eq)]
547struct StyleCascadeNarrowingSubstrateEntry {
548    style_path: String,
549    facts: OmenaQueryOmenaParserStyleFactsV0,
550    declarations: Vec<cascade_checker::QueryCheckerCascadeDeclaration>,
551}
552
553impl OmenaQueryStyleCascadeNarrowingSubstrateV0 {
554    fn declarations_for_style_path(
555        &self,
556        style_path: &str,
557    ) -> Option<&[cascade_checker::QueryCheckerCascadeDeclaration]> {
558        self.entries
559            .iter()
560            .find(|entry| entry.style_path == style_path)
561            .map(|entry| entry.declarations.as_slice())
562    }
563
564    pub(crate) fn visible_sass_symbol_keys_for_workspace_file(
565        &self,
566        target_style_path: &str,
567        package_manifests: &[OmenaQueryStylePackageManifestV0],
568        external_sifs: &[OmenaQueryExternalSifInputV0],
569        resolution_inputs: &OmenaQueryStyleResolutionInputsV0,
570    ) -> BTreeSet<diagnostics::SassSymbolKey> {
571        let facts_by_path = self
572            .entries
573            .iter()
574            .map(|entry| (entry.style_path.as_str(), &entry.facts))
575            .collect::<BTreeMap<_, _>>();
576        diagnostics::collect_visible_sass_symbol_keys(
577            target_style_path,
578            &facts_by_path,
579            &self.resolution,
580            diagnostics::OmenaQueryExternalSifResolutionContext {
581                package_manifests,
582                bundler_path_mappings: resolution_inputs.bundler_path_mappings.as_slice(),
583                tsconfig_path_mappings: resolution_inputs.tsconfig_path_mappings.as_slice(),
584                external_sifs,
585            },
586        )
587    }
588}
589
590pub fn collect_omena_query_style_cascade_narrowing_substrate(
591    style_sources: &[OmenaQueryStyleSourceInputV0],
592    package_manifests: &[OmenaQueryStylePackageManifestV0],
593    resolution_inputs: &OmenaQueryStyleResolutionInputsV0,
594) -> OmenaQueryStyleCascadeNarrowingSubstrateV0 {
595    collect_omena_query_style_cascade_narrowing_substrate_with_external_sifs(
596        style_sources,
597        package_manifests,
598        &[],
599        resolution_inputs,
600    )
601}
602
603pub fn collect_omena_query_style_cascade_narrowing_substrate_with_external_sifs(
604    style_sources: &[OmenaQueryStyleSourceInputV0],
605    package_manifests: &[OmenaQueryStylePackageManifestV0],
606    external_sifs: &[OmenaQueryExternalSifInputV0],
607    resolution_inputs: &OmenaQueryStyleResolutionInputsV0,
608) -> OmenaQueryStyleCascadeNarrowingSubstrateV0 {
609    #[cfg(feature = "salsa-memo")]
610    {
611        let mut host = OmenaQueryStyleMemoHostV0::new();
612        if let Some(selector) = host.workspace_revision_selector(
613            style_sources,
614            &[],
615            package_manifests,
616            external_sifs,
617            resolution_inputs,
618        ) {
619            return selector.style_cascade_narrowing_substrate();
620        }
621    }
622
623    let style_source_refs = style_sources
624        .iter()
625        .map(|source| (source.style_path.as_str(), source.style_source.as_str()))
626        .collect::<Vec<_>>();
627    let style_fact_entries = collect_omena_query_style_fact_entries(style_source_refs.as_slice());
628    let mut resolution = summarize_sass_module_cross_file_resolution(
629        &style_fact_entries,
630        package_manifests,
631        resolution_inputs.bundler_path_mappings.as_slice(),
632        resolution_inputs.tsconfig_path_mappings.as_slice(),
633    );
634    diagnostics::promote_sif_backed_external_edges(
635        &mut resolution,
636        diagnostics::OmenaQueryExternalSifResolutionContext {
637            package_manifests,
638            bundler_path_mappings: resolution_inputs.bundler_path_mappings.as_slice(),
639            tsconfig_path_mappings: resolution_inputs.tsconfig_path_mappings.as_slice(),
640            external_sifs,
641        },
642    );
643    let entries = style_sources
644        .iter()
645        .filter_map(|source| {
646            let facts = style_fact_entries
647                .iter()
648                .find(|entry| entry.style_path == source.style_path)
649                .map(|entry| entry.facts.clone())?;
650            Some(StyleCascadeNarrowingSubstrateEntry {
651                style_path: source.style_path.clone(),
652                facts,
653                declarations: cascade_checker::collect_query_checker_cascade_declarations(
654                    source.style_source.as_str(),
655                ),
656            })
657        })
658        .collect();
659    OmenaQueryStyleCascadeNarrowingSubstrateV0 {
660        entries,
661        resolution,
662    }
663}
664
665fn selector_property_value_narrowings_for_hover(
666    source: &str,
667    name: &str,
668    hovered_branch_scope: Option<&HoverCascadeBranchScope>,
669) -> Vec<AbstractPropertyValueNarrowingV0> {
670    let declarations = cascade_checker::collect_query_checker_cascade_declarations(source);
671    selector_property_value_narrowings_from_declarations(
672        declarations.as_slice(),
673        name,
674        hovered_branch_scope,
675    )
676}
677
678fn selector_property_value_narrowings_from_declarations(
679    declarations: &[cascade_checker::QueryCheckerCascadeDeclaration],
680    name: &str,
681    hovered_branch_scope: Option<&HoverCascadeBranchScope>,
682) -> Vec<AbstractPropertyValueNarrowingV0> {
683    let selector = format!(".{name}");
684    let matching_declarations = declarations
685        .iter()
686        .filter(|declaration| declaration.input.selector.as_str() == selector)
687        .collect::<Vec<_>>();
688    let mut branch_keys = matching_declarations
689        .iter()
690        .map(|declaration| {
691            (
692                declaration.input.property.clone(),
693                declaration.input.condition_context.clone(),
694                declaration.input.layer_name.clone(),
695                declaration.input.layer_order,
696            )
697        })
698        .collect::<BTreeSet<_>>()
699        .into_iter()
700        .filter(|(_, condition_context, _, _)| {
701            cascade_checker::query_condition_context_static_supports_pruning_evidence(
702                condition_context.as_slice(),
703                hovered_branch_scope.map(|scope| scope.condition_context.as_slice()),
704            )
705            .is_none_or(|evidence| !evidence.pruned)
706        })
707        .collect::<Vec<_>>();
708    branch_keys.sort();
709    if let Some(hovered_branch_scope) = hovered_branch_scope {
710        let filtered_branch_keys =
711            filter_hovered_branch_keys(branch_keys.as_slice(), hovered_branch_scope);
712        if !filtered_branch_keys.is_empty() {
713            branch_keys = filtered_branch_keys;
714        }
715    }
716
717    branch_keys
718        .into_iter()
719        .map(
720            |(property_name, condition_context, layer_name, layer_order)| {
721                let property_candidates = matching_declarations
722                    .iter()
723                    .filter(|declaration| declaration.input.property == property_name)
724                    .map(|declaration| AbstractPropertyValueCandidateV0 {
725                        property_name: declaration.input.property.clone(),
726                        value: declaration.input.value.clone(),
727                        pseudo_state: None,
728                        condition_context: declaration.input.condition_context.clone(),
729                        layer_name: declaration.input.layer_name.clone(),
730                        layer_order: declaration.input.layer_order,
731                        source_order: Some(declaration.input.source_order),
732                        important: declaration.input.important,
733                        same_selector_ordering: true,
734                    })
735                    .collect::<Vec<_>>();
736                narrow_abstract_property_value_for_cascade_branch(
737                    property_name.as_str(),
738                    None,
739                    condition_context.as_slice(),
740                    layer_name.as_deref(),
741                    layer_order,
742                    true,
743                    property_candidates.as_slice(),
744                )
745            },
746        )
747        .collect()
748}
749
750fn selector_property_value_narrowings_for_hover_module_graph(
751    target_style_path: &str,
752    style_sources: &[OmenaQueryStyleSourceInputV0],
753    package_manifests: &[OmenaQueryStylePackageManifestV0],
754    bundler_path_mappings: &[OmenaResolverBundlerPathAliasMappingV0],
755    tsconfig_path_mappings: &[OmenaResolverTsconfigPathMappingV0],
756    name: &str,
757    hovered_branch_scope: Option<&HoverCascadeBranchScope>,
758) -> Vec<AbstractPropertyValueNarrowingV0> {
759    let style_source_refs = style_sources
760        .iter()
761        .map(|source| (source.style_path.as_str(), source.style_source.as_str()))
762        .collect::<Vec<_>>();
763    let style_fact_entries = collect_omena_query_style_fact_entries(style_source_refs.as_slice());
764    let resolution = summarize_sass_module_cross_file_resolution(
765        &style_fact_entries,
766        package_manifests,
767        bundler_path_mappings,
768        tsconfig_path_mappings,
769    );
770    let reachable_style_paths = diagnostics::collect_sass_module_graph_reachable_style_paths(
771        target_style_path,
772        &resolution,
773    );
774    if reachable_style_paths.len() <= 1 {
775        return Vec::new();
776    }
777
778    let selector = format!(".{name}");
779    let collected_declarations = style_sources
780        .iter()
781        .filter(|source| reachable_style_paths.contains(source.style_path.as_str()))
782        .flat_map(|source| {
783            cascade_checker::collect_query_checker_cascade_declarations(
784                source.style_source.as_str(),
785            )
786        })
787        .filter(|declaration| declaration.input.selector.as_str() == selector)
788        .collect::<Vec<_>>();
789    let matching_declarations = collected_declarations.iter().collect::<Vec<_>>();
790    module_graph_narrowings_from_matching_declarations(
791        matching_declarations.as_slice(),
792        hovered_branch_scope,
793    )
794}
795
796fn selector_property_value_narrowings_for_hover_module_graph_with_substrate(
797    target_style_path: &str,
798    substrate: &OmenaQueryStyleCascadeNarrowingSubstrateV0,
799    name: &str,
800    hovered_branch_scope: Option<&HoverCascadeBranchScope>,
801) -> Vec<AbstractPropertyValueNarrowingV0> {
802    let reachable_style_paths = diagnostics::collect_sass_module_graph_reachable_style_paths(
803        target_style_path,
804        &substrate.resolution,
805    );
806    if reachable_style_paths.len() <= 1 {
807        return Vec::new();
808    }
809
810    let selector = format!(".{name}");
811    let matching_declarations = substrate
812        .entries
813        .iter()
814        .filter(|entry| reachable_style_paths.contains(entry.style_path.as_str()))
815        .flat_map(|entry| entry.declarations.iter())
816        .filter(|declaration| declaration.input.selector.as_str() == selector)
817        .collect::<Vec<_>>();
818    module_graph_narrowings_from_matching_declarations(
819        matching_declarations.as_slice(),
820        hovered_branch_scope,
821    )
822}
823
824fn module_graph_narrowings_from_matching_declarations(
825    matching_declarations: &[&cascade_checker::QueryCheckerCascadeDeclaration],
826    hovered_branch_scope: Option<&HoverCascadeBranchScope>,
827) -> Vec<AbstractPropertyValueNarrowingV0> {
828    if matching_declarations.is_empty() {
829        return Vec::new();
830    }
831
832    let mut branch_keys = matching_declarations
833        .iter()
834        .map(|declaration| {
835            (
836                declaration.input.property.clone(),
837                declaration.input.condition_context.clone(),
838                declaration.input.layer_name.clone(),
839                declaration.input.layer_order,
840            )
841        })
842        .collect::<BTreeSet<_>>()
843        .into_iter()
844        .filter(|(_, condition_context, _, _)| {
845            cascade_checker::query_condition_context_static_supports_pruning_evidence(
846                condition_context.as_slice(),
847                hovered_branch_scope.map(|scope| scope.condition_context.as_slice()),
848            )
849            .is_none_or(|evidence| !evidence.pruned)
850        })
851        .collect::<Vec<_>>();
852    branch_keys.sort();
853    if let Some(hovered_branch_scope) = hovered_branch_scope {
854        let filtered_branch_keys =
855            filter_hovered_branch_keys(branch_keys.as_slice(), hovered_branch_scope);
856        if !filtered_branch_keys.is_empty() {
857            branch_keys = filtered_branch_keys;
858        }
859    }
860
861    branch_keys
862        .into_iter()
863        .map(
864            |(property_name, condition_context, layer_name, layer_order)| {
865                let property_candidates = matching_declarations
866                    .iter()
867                    .filter(|declaration| declaration.input.property == property_name)
868                    .map(|declaration| AbstractPropertyValueCandidateV0 {
869                        property_name: declaration.input.property.clone(),
870                        value: declaration.input.value.clone(),
871                        pseudo_state: None,
872                        condition_context: declaration.input.condition_context.clone(),
873                        layer_name: declaration.input.layer_name.clone(),
874                        layer_order: declaration.input.layer_order,
875                        source_order: Some(declaration.input.source_order),
876                        important: declaration.input.important,
877                        same_selector_ordering: false,
878                    })
879                    .collect::<Vec<_>>();
880                let mut narrowed = narrow_abstract_property_value_for_cascade_branch(
881                    property_name.as_str(),
882                    None,
883                    condition_context.as_slice(),
884                    layer_name.as_deref(),
885                    layer_order,
886                    true,
887                    property_candidates.as_slice(),
888                );
889                narrowed.stylesheet_scope = "moduleGraph";
890                narrowed
891            },
892        )
893        .collect()
894}
895
896fn filter_hovered_branch_keys(
897    branch_keys: &[SelectorPropertyBranchKey],
898    hovered_branch_scope: &HoverCascadeBranchScope,
899) -> Vec<SelectorPropertyBranchKey> {
900    branch_keys
901        .iter()
902        .filter(|(_, condition_context, layer_name, layer_order)| {
903            condition_context == &hovered_branch_scope.condition_context
904                && layer_name == &hovered_branch_scope.layer_name
905                && layer_order == &hovered_branch_scope.layer_order
906        })
907        .cloned()
908        .collect()
909}
910
911fn selector_hover_branch_scope_at_position(
912    source: &str,
913    name: &str,
914    position: ParserPositionV0,
915) -> Option<HoverCascadeBranchScope> {
916    let offset = byte_offset_for_parser_position(source, position)?;
917    let selector = format!(".{name}");
918    let mut layer_orders = BTreeMap::new();
919    let mut next_layer_order = 0i32;
920    let mut matches = Vec::new();
921    collect_hover_selector_branch_scopes(
922        source,
923        0,
924        source.len(),
925        None,
926        Vec::new(),
927        None,
928        None,
929        &mut layer_orders,
930        &mut next_layer_order,
931        selector.as_str(),
932        offset,
933        &mut matches,
934    );
935    matches.sort();
936    matches.into_iter().next().map(|matched| matched.scope)
937}
938
939#[allow(clippy::too_many_arguments)]
940fn collect_hover_selector_branch_scopes(
941    source: &str,
942    start: usize,
943    end: usize,
944    parent_selector: Option<String>,
945    condition_context: Vec<String>,
946    layer_name: Option<String>,
947    layer_order: Option<i32>,
948    layer_orders: &mut BTreeMap<String, i32>,
949    next_layer_order: &mut i32,
950    target_selector: &str,
951    hover_offset: usize,
952    matches: &mut Vec<HoverCascadeBranchMatch>,
953) {
954    let mut index = start;
955    while let Some(open_index) = find_hover_style_top_level_byte(source, index, end, b'{') {
956        let Some(close_index) = matching_style_block_end(source, open_index, b'{', b'}') else {
957            break;
958        };
959        if close_index > end {
960            break;
961        }
962        let prelude_start = hover_style_prelude_start(source, start, open_index);
963        let prelude = source[prelude_start..open_index].trim();
964        let body_start = open_index + 1;
965
966        if let Some(layer) = hover_layer_name_from_prelude(prelude) {
967            let order = *layer_orders.entry(layer.clone()).or_insert_with(|| {
968                let order = *next_layer_order;
969                *next_layer_order += 1;
970                order
971            });
972            collect_hover_selector_branch_scopes(
973                source,
974                body_start,
975                close_index,
976                parent_selector.clone(),
977                condition_context.clone(),
978                Some(layer),
979                Some(order),
980                layer_orders,
981                next_layer_order,
982                target_selector,
983                hover_offset,
984                matches,
985            );
986        } else if prelude.starts_with('@') {
987            let mut nested_condition_context = condition_context.clone();
988            nested_condition_context.push(normalize_hover_condition_prelude(prelude));
989            collect_hover_selector_branch_scopes(
990                source,
991                body_start,
992                close_index,
993                parent_selector.clone(),
994                nested_condition_context,
995                layer_name.clone(),
996                layer_order,
997                layer_orders,
998                next_layer_order,
999                target_selector,
1000                hover_offset,
1001                matches,
1002            );
1003        } else if !prelude.is_empty() {
1004            let canonical_members = split_hover_selector_list(prelude)
1005                .into_iter()
1006                .map(|member| canonical_hover_selector(parent_selector.as_deref(), member.as_str()))
1007                .collect::<Vec<_>>();
1008            if canonical_members
1009                .iter()
1010                .any(|member| member == target_selector)
1011                && hover_offset >= prelude_start
1012                && hover_offset <= close_index
1013            {
1014                matches.push(HoverCascadeBranchMatch {
1015                    span_len: close_index.saturating_sub(prelude_start),
1016                    scope: HoverCascadeBranchScope {
1017                        condition_context: condition_context.clone(),
1018                        layer_name: layer_name.clone(),
1019                        layer_order,
1020                    },
1021                });
1022            }
1023            for canonical_selector in canonical_members {
1024                collect_hover_selector_branch_scopes(
1025                    source,
1026                    body_start,
1027                    close_index,
1028                    Some(canonical_selector),
1029                    condition_context.clone(),
1030                    layer_name.clone(),
1031                    layer_order,
1032                    layer_orders,
1033                    next_layer_order,
1034                    target_selector,
1035                    hover_offset,
1036                    matches,
1037                );
1038            }
1039        }
1040
1041        index = close_index + 1;
1042    }
1043}
1044
1045fn find_hover_style_top_level_byte(
1046    source: &str,
1047    start: usize,
1048    end: usize,
1049    needle: u8,
1050) -> Option<usize> {
1051    let mut index = start;
1052    let mut quote: Option<u8> = None;
1053    let mut paren_depth = 0usize;
1054    while index < end {
1055        let byte = source.as_bytes().get(index).copied()?;
1056        if let Some(quote_byte) = quote {
1057            if byte == b'\\' {
1058                index = advance_style_escaped_char(source, index, end);
1059            } else if byte == quote_byte {
1060                quote = None;
1061                index = advance_style_scan_cursor(source, index, end);
1062            } else {
1063                index = advance_style_scan_cursor(source, index, end);
1064            }
1065            continue;
1066        }
1067        if source[index..end].starts_with("/*")
1068            && let Some(close_offset) = source[index + 2..end].find("*/")
1069        {
1070            index += close_offset + 4;
1071            continue;
1072        }
1073        if byte == needle && paren_depth == 0 {
1074            return Some(index);
1075        }
1076        match byte {
1077            b'"' | b'\'' | b'`' => {
1078                quote = Some(byte);
1079                index = advance_style_scan_cursor(source, index, end);
1080            }
1081            b'(' => {
1082                paren_depth += 1;
1083                index = advance_style_scan_cursor(source, index, end);
1084            }
1085            b')' => {
1086                paren_depth = paren_depth.saturating_sub(1);
1087                index = advance_style_scan_cursor(source, index, end);
1088            }
1089            _ => index = advance_style_scan_cursor(source, index, end),
1090        }
1091    }
1092    None
1093}
1094
1095fn hover_style_prelude_start(source: &str, search_start: usize, open_index: usize) -> usize {
1096    source[search_start..open_index]
1097        .rfind(['{', '}', ';'])
1098        .map(|offset| search_start + offset + 1)
1099        .unwrap_or(search_start)
1100}
1101
1102fn hover_layer_name_from_prelude(prelude: &str) -> Option<String> {
1103    let rest = css_keyword(prelude.trim_start())
1104        .strip_prefix("@layer")?
1105        .trim();
1106    let name = rest
1107        .split(|ch: char| ch.is_ascii_whitespace() || matches!(ch, ',' | '{' | ';'))
1108        .next()
1109        .unwrap_or_default()
1110        .trim_matches(['"', '\'']);
1111    if name.is_empty() {
1112        Some("(anonymous-layer)".to_string())
1113    } else {
1114        Some(name.to_string())
1115    }
1116}
1117
1118fn normalize_hover_condition_prelude(prelude: &str) -> String {
1119    prelude.split_whitespace().collect::<Vec<_>>().join(" ")
1120}
1121
1122fn split_hover_selector_list(prelude: &str) -> Vec<String> {
1123    let mut members = split_top_level_style_segments(prelude, 0, prelude.len(), b',')
1124        .into_iter()
1125        .filter_map(|(start, end)| {
1126            let member = prelude[start..end].trim();
1127            (!member.is_empty()).then(|| member.to_string())
1128        })
1129        .collect::<Vec<_>>();
1130    if members.is_empty() {
1131        members.push(prelude.trim().to_string());
1132    }
1133    members
1134}
1135
1136fn canonical_hover_selector(parent_selector: Option<&str>, selector: &str) -> String {
1137    let selector = selector.trim();
1138    match parent_selector {
1139        Some(parent_selector) if selector.contains('&') => selector.replace('&', parent_selector),
1140        Some(parent_selector) => format!("{parent_selector} {selector}"),
1141        None => selector.to_string(),
1142    }
1143}
1144
1145fn source_reference_text_selector_name(source: &str, span: ParserByteSpanV0) -> Option<String> {
1146    let text = source.get(span.start..span.end)?;
1147    if text.is_empty() {
1148        return None;
1149    }
1150    text.chars()
1151        .all(is_css_identifier_continue)
1152        .then(|| text.to_string())
1153}
1154
1155pub fn summarize_omena_query_style_semantic_graph_batch_from_sources<'a>(
1156    styles: impl IntoIterator<Item = (&'a str, &'a str)>,
1157    input: &EngineInputV2,
1158) -> OmenaQueryStyleSemanticGraphBatchOutputV0 {
1159    summarize_omena_query_style_semantic_graph_batch_from_sources_with_package_manifests(
1160        styles,
1161        input,
1162        &[],
1163    )
1164}
1165
1166pub fn summarize_omena_query_style_semantic_graph_batch_from_sources_with_package_manifests<'a>(
1167    styles: impl IntoIterator<Item = (&'a str, &'a str)>,
1168    input: &EngineInputV2,
1169    package_manifests: &[OmenaQueryStylePackageManifestV0],
1170) -> OmenaQueryStyleSemanticGraphBatchOutputV0 {
1171    let resolution_inputs = OmenaQueryStyleResolutionInputsV0 {
1172        package_manifests: package_manifests.to_vec(),
1173        ..OmenaQueryStyleResolutionInputsV0::default()
1174    };
1175    summarize_omena_query_style_semantic_graph_batch_from_sources_with_resolution_inputs(
1176        styles,
1177        input,
1178        package_manifests,
1179        &resolution_inputs,
1180    )
1181}
1182
1183pub fn summarize_omena_query_style_semantic_graph_batch_from_sources_with_resolution_inputs<'a>(
1184    styles: impl IntoIterator<Item = (&'a str, &'a str)>,
1185    input: &EngineInputV2,
1186    package_manifests: &[OmenaQueryStylePackageManifestV0],
1187    resolution_inputs: &OmenaQueryStyleResolutionInputsV0,
1188) -> OmenaQueryStyleSemanticGraphBatchOutputV0 {
1189    let style_sources = styles
1190        .into_iter()
1191        .map(|(style_path, style_source)| OmenaQueryStyleSourceInputV0 {
1192            style_path: style_path.to_string(),
1193            style_source: style_source.to_string(),
1194        })
1195        .collect::<Vec<_>>();
1196    let style_source_refs = style_sources
1197        .iter()
1198        .map(|source| (source.style_path.as_str(), source.style_source.as_str()))
1199        .collect::<Vec<_>>();
1200
1201    #[cfg(feature = "salsa-memo")]
1202    {
1203        let mut host = OmenaQueryStyleMemoHostV0::new();
1204        if let Some(selector) = host.workspace_revision_selector(
1205            style_sources.as_slice(),
1206            &[],
1207            package_manifests,
1208            &[],
1209            resolution_inputs,
1210        ) {
1211            return selector.style_semantic_graph_batch(input, package_manifests);
1212        }
1213    }
1214
1215    let style_fact_entries = collect_omena_query_style_fact_entries(style_source_refs.as_slice());
1216    let css_modules_resolution = summarize_css_modules_cross_file_resolution_with_resolution_inputs(
1217        &style_fact_entries,
1218        package_manifests,
1219        resolution_inputs,
1220    );
1221    let sass_module_resolution = summarize_sass_module_cross_file_resolution(
1222        &style_fact_entries,
1223        package_manifests,
1224        &[],
1225        &[],
1226    );
1227    let cross_file_summary = summarize_omena_query_cross_file_summary(
1228        &style_fact_entries,
1229        &css_modules_resolution,
1230        &sass_module_resolution,
1231    );
1232    summarize_omena_query_style_semantic_graph_batch_from_committed_parts(
1233        style_sources.as_slice(),
1234        input,
1235        package_manifests,
1236        resolution_inputs,
1237        OmenaQueryStyleSemanticGraphCommittedParts {
1238            style_fact_entries: style_fact_entries.as_slice(),
1239            cross_file_summary,
1240            css_modules_resolution,
1241            sass_module_resolution,
1242        },
1243    )
1244}
1245
1246pub(in crate::style) struct OmenaQueryStyleSemanticGraphCommittedParts<'a> {
1247    pub style_fact_entries: &'a [OmenaQueryStyleFactEntry],
1248    pub cross_file_summary: OmenaQueryCrossFileSummaryV0,
1249    pub css_modules_resolution: OmenaQueryCssModulesCrossFileResolutionV0,
1250    pub sass_module_resolution: OmenaQuerySassModuleCrossFileResolutionV0,
1251}
1252
1253pub(in crate::style) fn summarize_omena_query_style_semantic_graph_batch_from_committed_parts(
1254    style_sources: &[OmenaQueryStyleSourceInputV0],
1255    input: &EngineInputV2,
1256    package_manifests: &[OmenaQueryStylePackageManifestV0],
1257    resolution_inputs: &OmenaQueryStyleResolutionInputsV0,
1258    committed_parts: OmenaQueryStyleSemanticGraphCommittedParts<'_>,
1259) -> OmenaQueryStyleSemanticGraphBatchOutputV0 {
1260    let OmenaQueryStyleSemanticGraphCommittedParts {
1261        style_fact_entries,
1262        cross_file_summary,
1263        css_modules_resolution,
1264        sass_module_resolution,
1265    } = committed_parts;
1266    let workspace_declarations = style_fact_entries
1267        .iter()
1268        .flat_map(|entry| {
1269            collect_omena_bridge_design_token_workspace_declarations_from_source(
1270                entry.style_path.as_str(),
1271                entry.style_source.as_str(),
1272            )
1273        })
1274        .collect::<Vec<_>>();
1275    let graphs = style_sources
1276        .iter()
1277        .map(|source| OmenaQueryStyleSemanticGraphBatchEntryV0 {
1278                style_path: source.style_path.clone(),
1279                graph: {
1280                    let import_reachable_declarations =
1281                        filter_import_reachable_design_token_workspace_declarations(
1282                            source.style_path.as_str(),
1283                            style_fact_entries,
1284                            &workspace_declarations,
1285                            package_manifests,
1286                            resolution_inputs.bundler_path_mappings.as_slice(),
1287                            resolution_inputs.tsconfig_path_mappings.as_slice(),
1288                            resolution_inputs.disk_style_path_identities.as_slice(),
1289                        );
1290                    summarize_omena_bridge_style_semantic_graph_from_source_with_scoped_workspace_declarations(
1291                        source.style_path.as_str(),
1292                        source.style_source.as_str(),
1293                        input,
1294                        &import_reachable_declarations,
1295                        DesignTokenExternalDeclarationCandidateScopeV0::CrossFileImportGraph,
1296                    )
1297                },
1298            })
1299        .collect::<Vec<_>>();
1300
1301    OmenaQueryStyleSemanticGraphBatchOutputV0 {
1302        schema_version: "0",
1303        product: "omena-semantic.style-semantic-graph-batch",
1304        cross_file_summary,
1305        css_modules_resolution,
1306        sass_module_resolution,
1307        graphs,
1308    }
1309}
1310
1311#[derive(Debug, Clone, PartialEq, Eq)]
1312struct OmenaQueryStyleFactEntry {
1313    style_path: String,
1314    style_source: String,
1315    facts: OmenaQueryOmenaParserStyleFactsV0,
1316    icss_export_values: BTreeMap<String, String>,
1317    semantic_runtime_index: Option<omena_semantic::StyleRuntimeIndexFactsV0>,
1318}
1319
1320#[derive(Debug, Clone, PartialEq, Eq)]
1321pub struct OmenaQueryModuleInterfaceProjectionV0 {
1322    pub style_path: String,
1323    pub style_selector_definitions: Vec<OmenaQueryStyleSelectorDefinitionV0>,
1324    pub css_modules_style_facts: omena_semantic::CssModulesCrossFileStyleFactsV0,
1325    pub custom_property_decl_names: BTreeSet<String>,
1326    pub custom_property_ref_names: Vec<String>,
1327    pub style_dependency_sources: Vec<String>,
1328    pub sass_module_edges: Vec<OmenaQuerySassModuleEdgeFactV0>,
1329    pub sass_module_configurable_variable_names: BTreeSet<String>,
1330    pub sass_module_rule_configurations: Vec<OmenaQuerySassModuleRuleConfigurationSurfaceV0>,
1331}
1332
1333#[derive(Debug, Clone, PartialEq, Eq)]
1334pub struct OmenaQuerySassModuleRuleConfigurationSurfaceV0 {
1335    pub edge_kind: &'static str,
1336    pub rule_ordinal: usize,
1337    pub variable_overrides: BTreeMap<String, String>,
1338    pub forward_variable_overrides: BTreeMap<String, omena_semantic::SassModuleVariableOverrideV0>,
1339}
1340
1341pub fn summarize_omena_query_sass_module_cross_file_resolution_for_workspace(
1342    style_sources: &[OmenaQueryStyleSourceInputV0],
1343    package_manifests: &[OmenaQueryStylePackageManifestV0],
1344    bundler_path_mappings: &[OmenaResolverBundlerPathAliasMappingV0],
1345    tsconfig_path_mappings: &[OmenaResolverTsconfigPathMappingV0],
1346) -> OmenaQuerySassModuleCrossFileResolutionV0 {
1347    #[cfg(feature = "salsa-memo")]
1348    {
1349        let mut host = OmenaQueryStyleMemoHostV0::new();
1350        let resolution_inputs = OmenaQueryStyleResolutionInputsV0 {
1351            package_manifests: package_manifests.to_vec(),
1352            tsconfig_path_mappings: tsconfig_path_mappings.to_vec(),
1353            bundler_path_mappings: bundler_path_mappings.to_vec(),
1354            ..OmenaQueryStyleResolutionInputsV0::default()
1355        };
1356        if let Some(selector) = host.workspace_revision_selector(
1357            style_sources,
1358            &[],
1359            package_manifests,
1360            &[],
1361            &resolution_inputs,
1362        ) {
1363            return selector.sass_module_cross_file_resolution().clone();
1364        }
1365    }
1366
1367    #[cfg(any(test, feature = "test-support"))]
1368    record_sass_module_resolution_direct_recompute_for_test();
1369
1370    let style_source_refs = style_sources
1371        .iter()
1372        .map(|source| (source.style_path.as_str(), source.style_source.as_str()))
1373        .collect::<Vec<_>>();
1374    let style_fact_entries = collect_omena_query_style_fact_entries(style_source_refs.as_slice());
1375    summarize_sass_module_cross_file_resolution(
1376        &style_fact_entries,
1377        package_manifests,
1378        bundler_path_mappings,
1379        tsconfig_path_mappings,
1380    )
1381}
1382
1383fn collect_omena_query_style_fact_entries(
1384    style_sources: &[(&str, &str)],
1385) -> Vec<OmenaQueryStyleFactEntry> {
1386    style_sources
1387        .iter()
1388        .map(|(style_path, style_source)| {
1389            collect_omena_query_style_fact_entry(style_path, style_source)
1390        })
1391        .collect()
1392}
1393
1394fn collect_omena_query_style_fact_entry(
1395    style_path: &str,
1396    style_source: &str,
1397) -> OmenaQueryStyleFactEntry {
1398    let dialect = omena_parser_dialect_for_style_path(style_path);
1399    let (raw_facts, icss_export_values) =
1400        collect_omena_query_style_facts_with_icss_values_raw(style_source, dialect);
1401    let facts = summarize_omena_query_omena_parser_style_facts_from_facts(raw_facts, dialect);
1402    let semantic_runtime_index = semantic_runtime_index_from_query_style_facts(style_path, &facts);
1403    OmenaQueryStyleFactEntry {
1404        style_path: style_path.to_string(),
1405        style_source: style_source.to_string(),
1406        semantic_runtime_index,
1407        facts,
1408        icss_export_values,
1409    }
1410}
1411
1412/// Per-document module-interface projection from source text alone — the
1413/// SAME projection the workspace transaction compares to populate
1414/// `changed_module_interface_paths`, so callers can decide "this edit
1415/// preserved the module interface" from one single-file parse instead of a
1416/// workspace selector build.
1417pub fn summarize_omena_query_module_interface_projection(
1418    style_path: &str,
1419    style_source: &str,
1420) -> OmenaQueryModuleInterfaceProjectionV0 {
1421    module_interface_projection_for_query(&collect_omena_query_style_fact_entry(
1422        style_path,
1423        style_source,
1424    ))
1425}
1426
1427pub fn summarize_omena_query_css_modules_interface_bundle(
1428    style_sources: &[OmenaQueryStyleSourceInputV0],
1429    package_manifests: &[OmenaQueryStylePackageManifestV0],
1430) -> OmenaQueryCssModulesInterfaceBundleV0 {
1431    let style_source_refs = style_sources
1432        .iter()
1433        .map(|source| (source.style_path.as_str(), source.style_source.as_str()))
1434        .collect::<Vec<_>>();
1435    let entries = collect_omena_query_style_fact_entries(style_source_refs.as_slice());
1436    let projections = entries
1437        .iter()
1438        .map(module_interface_projection_for_query)
1439        .collect::<Vec<_>>();
1440    let icss_export_values_by_path = entries
1441        .iter()
1442        .map(|entry| (entry.style_path.clone(), entry.icss_export_values.clone()))
1443        .collect::<BTreeMap<_, _>>();
1444    let emitted_class_names = entries
1445        .iter()
1446        .flat_map(|entry| {
1447            transform::derive_class_name_rewrites_for_transform_context(entry)
1448                .into_iter()
1449                .map(|rewrite| {
1450                    (
1451                        (entry.style_path.clone(), rewrite.original_name),
1452                        rewrite.rewritten_name,
1453                    )
1454                })
1455        })
1456        .collect::<BTreeMap<_, _>>();
1457    let resolution = summarize_css_modules_cross_file_resolution(&entries, package_manifests);
1458    summarize_css_modules_interface_bundle_from_projections(
1459        &projections,
1460        &resolution,
1461        &icss_export_values_by_path,
1462        &emitted_class_names,
1463    )
1464}
1465
1466fn module_interface_projection_for_query(
1467    entry: &OmenaQueryStyleFactEntry,
1468) -> OmenaQueryModuleInterfaceProjectionV0 {
1469    OmenaQueryModuleInterfaceProjectionV0 {
1470        style_path: entry.style_path.clone(),
1471        style_selector_definitions: style_selector_definitions_for_query(entry),
1472        css_modules_style_facts: css_modules_cross_file_style_fact_for_query(entry),
1473        custom_property_decl_names: custom_property_decl_names_for_query(entry),
1474        custom_property_ref_names: custom_property_ref_names_for_query(entry),
1475        style_dependency_sources: collect_style_module_dependency_sources_from_facts(&entry.facts),
1476        sass_module_edges: entry.facts.sass_module_edges.clone(),
1477        sass_module_configurable_variable_names: sass_module_configurable_variable_names_for_query(
1478            entry,
1479        ),
1480        sass_module_rule_configurations: sass_module_rule_configuration_surfaces_for_query(entry),
1481    }
1482}
1483
1484fn style_selector_definitions_for_query(
1485    entry: &OmenaQueryStyleFactEntry,
1486) -> Vec<OmenaQueryStyleSelectorDefinitionV0> {
1487    let Some(candidates) = summarize_omena_query_style_hover_candidates(
1488        entry.style_path.as_str(),
1489        entry.style_source.as_str(),
1490    ) else {
1491        return Vec::new();
1492    };
1493    let mut definitions = candidates
1494        .candidates
1495        .into_iter()
1496        .filter_map(|candidate| {
1497            (candidate.kind == "selector").then(|| OmenaQueryStyleSelectorDefinitionV0 {
1498                uri: entry.style_path.clone(),
1499                name: candidate.name,
1500                range: candidate.range,
1501            })
1502        })
1503        .collect::<Vec<_>>();
1504    definitions.sort_by_key(|definition| {
1505        (
1506            definition.uri.clone(),
1507            definition.range.start.line,
1508            definition.range.start.character,
1509            definition.name.clone(),
1510        )
1511    });
1512    definitions.dedup_by(|left, right| {
1513        left.uri == right.uri && left.name == right.name && left.range == right.range
1514    });
1515    definitions
1516}
1517
1518fn custom_property_decl_names_for_query(entry: &OmenaQueryStyleFactEntry) -> BTreeSet<String> {
1519    entry
1520        .semantic_runtime_index
1521        .as_ref()
1522        .map(|index| index.custom_property_decl_names.iter().cloned().collect())
1523        .unwrap_or_else(|| {
1524            entry
1525                .facts
1526                .custom_property_decl_names
1527                .iter()
1528                .cloned()
1529                .collect()
1530        })
1531}
1532
1533fn custom_property_ref_names_for_query(entry: &OmenaQueryStyleFactEntry) -> Vec<String> {
1534    entry
1535        .semantic_runtime_index
1536        .as_ref()
1537        .map(|index| index.custom_property_ref_names.clone())
1538        .unwrap_or_else(|| entry.facts.custom_property_ref_names.clone())
1539}
1540
1541fn sass_module_configurable_variable_names_for_query(
1542    entry: &OmenaQueryStyleFactEntry,
1543) -> BTreeSet<String> {
1544    #[cfg(test)]
1545    CONFIGURABLE_NAMES_DERIVATIONS.with(|count| count.set(count.get() + 1));
1546    stylesheet_evaluation::derive_static_scss_stylesheet_module_configurable_variable_names(
1547        &entry.style_source,
1548    )
1549}
1550
1551fn sass_module_rule_configuration_surfaces_for_query(
1552    entry: &OmenaQueryStyleFactEntry,
1553) -> Vec<OmenaQuerySassModuleRuleConfigurationSurfaceV0> {
1554    let mut surfaces = Vec::new();
1555    let mut sass_use_rule_ordinal = 0usize;
1556    let mut sass_forward_rule_ordinal = 0usize;
1557    for edge in &entry.facts.sass_module_edges {
1558        match edge.kind {
1559            "sassUse" => {
1560                surfaces.push(OmenaQuerySassModuleRuleConfigurationSurfaceV0 {
1561                    edge_kind: edge.kind,
1562                    rule_ordinal: sass_use_rule_ordinal,
1563                    variable_overrides:
1564                        omena_semantic::derive_sass_module_rule_variable_overrides_at_ordinal(
1565                            entry.style_source.as_str(),
1566                            "@use",
1567                            sass_use_rule_ordinal,
1568                        ),
1569                    forward_variable_overrides: BTreeMap::new(),
1570                });
1571                sass_use_rule_ordinal += 1;
1572            }
1573            "sassForward" => {
1574                let forward_variable_overrides =
1575                    omena_semantic::derive_sass_module_forward_variable_overrides_at_ordinal(
1576                        entry.style_source.as_str(),
1577                        sass_forward_rule_ordinal,
1578                    );
1579                surfaces.push(OmenaQuerySassModuleRuleConfigurationSurfaceV0 {
1580                    edge_kind: edge.kind,
1581                    rule_ordinal: sass_forward_rule_ordinal,
1582                    variable_overrides: forward_variable_overrides
1583                        .iter()
1584                        .map(|(name, override_entry)| (name.clone(), override_entry.value.clone()))
1585                        .collect(),
1586                    forward_variable_overrides,
1587                });
1588                sass_forward_rule_ordinal += 1;
1589            }
1590            _ => {}
1591        }
1592    }
1593    surfaces
1594}
1595
1596fn semantic_runtime_index_from_query_style_facts(
1597    style_path: &str,
1598    facts: &OmenaQueryOmenaParserStyleFactsV0,
1599) -> Option<omena_semantic::StyleRuntimeIndexFactsV0> {
1600    let language = semantic_runtime_index_language_for_style_path(style_path)?;
1601    Some(omena_semantic::StyleRuntimeIndexFactsV0 {
1602        schema_version: "0",
1603        product: "omena-semantic.style-runtime-index-facts",
1604        style_path: style_path.to_string(),
1605        language,
1606        class_selector_names: facts.class_selector_names.clone(),
1607        custom_property_names: facts.custom_property_names.clone(),
1608        custom_property_decl_names: facts.custom_property_decl_names.clone(),
1609        custom_property_ref_names: facts.custom_property_ref_names.clone(),
1610        keyframe_names: facts.keyframe_names.clone(),
1611        animation_reference_names: facts.animation_reference_names.clone(),
1612        ready_surfaces: vec![
1613            "semanticRuntimeIndexFacts",
1614            "customPropertyRuntimeIndex",
1615            "keyframeRuntimeIndex",
1616        ],
1617    })
1618}
1619
1620fn semantic_runtime_index_language_for_style_path(style_path: &str) -> Option<&'static str> {
1621    if style_path.ends_with(".module.css") || style_path.ends_with(".css") {
1622        Some("css")
1623    } else if style_path.ends_with(".module.scss") || style_path.ends_with(".scss") {
1624        Some("scss")
1625    } else if style_path.ends_with(".module.sass") || style_path.ends_with(".sass") {
1626        Some("sass")
1627    } else if style_path.ends_with(".module.less") || style_path.ends_with(".less") {
1628        Some("less")
1629    } else {
1630        None
1631    }
1632}
1633
1634#[cfg(any(test, feature = "test-support"))]
1635thread_local! {
1636    static SASS_MODULE_RESOLUTION_DIRECT_RECOMPUTES: std::cell::Cell<u64> =
1637        const { std::cell::Cell::new(0) };
1638    static SASS_MODULE_RESOLUTION_INTERNAL_COMPUTES: std::cell::Cell<u64> =
1639        const { std::cell::Cell::new(0) };
1640}
1641
1642#[cfg(any(test, feature = "test-support"))]
1643pub fn reset_sass_module_resolution_direct_recompute_count_for_test() {
1644    SASS_MODULE_RESOLUTION_DIRECT_RECOMPUTES.with(|count| count.set(0));
1645}
1646
1647#[cfg(any(test, feature = "test-support"))]
1648pub fn reset_sass_module_resolution_internal_compute_count_for_test() {
1649    SASS_MODULE_RESOLUTION_INTERNAL_COMPUTES.with(|count| count.set(0));
1650}
1651
1652#[cfg(any(test, feature = "test-support"))]
1653pub fn read_sass_module_resolution_direct_recompute_count_for_test() -> u64 {
1654    SASS_MODULE_RESOLUTION_DIRECT_RECOMPUTES.with(|count| count.get())
1655}
1656
1657#[cfg(any(test, feature = "test-support"))]
1658pub fn read_sass_module_resolution_internal_compute_count_for_test() -> u64 {
1659    SASS_MODULE_RESOLUTION_INTERNAL_COMPUTES.with(|count| count.get())
1660}
1661
1662#[cfg(any(test, feature = "test-support"))]
1663fn record_sass_module_resolution_direct_recompute_for_test() {
1664    SASS_MODULE_RESOLUTION_DIRECT_RECOMPUTES.with(|count| {
1665        count.set(count.get() + 1);
1666    });
1667}
1668
1669#[cfg(any(test, feature = "test-support"))]
1670fn record_sass_module_resolution_internal_compute_for_test() {
1671    SASS_MODULE_RESOLUTION_INTERNAL_COMPUTES.with(|count| {
1672        count.set(count.get() + 1);
1673    });
1674}
1675
1676/// Derive the load-path roots to try when joining a load-path-rooted `@use` (dart-sass
1677/// `--load-path`). Each in-graph style file contributes its ancestor directories: a path-shaped
1678/// specifier `src/scss/design-system.scss` is then joinable under any root `<R>` for which
1679/// `<R>/src/scss/design-system.scss` is itself in-graph. The resolver accepts only such existing
1680/// candidates, so over-collecting roots cannot fabricate a spurious edge. (RFC-0007-I, #49)
1681fn collect_load_path_roots(available_style_paths: &BTreeSet<&str>) -> Vec<String> {
1682    let mut roots = BTreeSet::new();
1683    for path in available_style_paths {
1684        let mut current = *path;
1685        // Walk up the directory chain on the normalized `/` separator. Style paths flowing
1686        // through the query layer are already forward-slash normalized by the resolver.
1687        while let Some(parent_end) = current.rfind('/') {
1688            if parent_end == 0 {
1689                // Keep the filesystem root (`/`) as a candidate load-path root.
1690                roots.insert("/".to_string());
1691                break;
1692            }
1693            let parent = &current[..parent_end];
1694            if !roots.insert(parent.to_string()) {
1695                // This ancestor (and therefore all of its ancestors) is already recorded.
1696                break;
1697            }
1698            current = parent;
1699        }
1700    }
1701    roots.into_iter().collect()
1702}
1703
1704fn summarize_sass_module_cross_file_resolution(
1705    style_fact_entries: &[OmenaQueryStyleFactEntry],
1706    package_manifests: &[OmenaQueryStylePackageManifestV0],
1707    bundler_path_mappings: &[OmenaResolverBundlerPathAliasMappingV0],
1708    tsconfig_path_mappings: &[OmenaResolverTsconfigPathMappingV0],
1709) -> OmenaQuerySassModuleCrossFileResolutionV0 {
1710    #[cfg(any(test, feature = "test-support"))]
1711    record_sass_module_resolution_internal_compute_for_test();
1712
1713    let available_style_paths = style_fact_entries
1714        .iter()
1715        .map(|entry| entry.style_path.as_str())
1716        .collect::<BTreeSet<_>>();
1717    let resolver_available_style_paths = style_fact_entries
1718        .iter()
1719        .flat_map(|entry| {
1720            [
1721                entry.style_path.clone(),
1722                resolver_style_path(entry.style_path.as_str()),
1723            ]
1724        })
1725        .collect::<BTreeSet<_>>();
1726    let resolver_available_style_path_refs = resolver_available_style_paths
1727        .iter()
1728        .map(String::as_str)
1729        .collect::<BTreeSet<_>>();
1730    // Load-path roots are the ancestor directories of the in-graph style files. A
1731    // load-path-rooted `@use 'src/scss/design-system.scss'` (dart-sass `--load-path`) is joined
1732    // only when `<root>/src/scss/design-system.scss` is itself an in-graph file, so deriving
1733    // roots from `available_style_paths` keeps the join sound without new configuration input,
1734    // and never shadows the file-relative or bare-package routes. (RFC-0007-I, #49)
1735    let load_path_roots = collect_load_path_roots(&resolver_available_style_path_refs);
1736    let load_path_root_refs = load_path_roots
1737        .iter()
1738        .map(String::as_str)
1739        .collect::<Vec<_>>();
1740    let resolver_package_manifests = package_manifests
1741        .iter()
1742        .map(|manifest| OmenaResolverStylePackageManifestV0 {
1743            package_json_path: manifest.package_json_path.clone(),
1744            package_json_source: manifest.package_json_source.clone(),
1745        })
1746        .collect::<Vec<_>>();
1747    let source_by_path = style_fact_entries
1748        .iter()
1749        .map(|entry| (entry.style_path.clone(), entry.style_source.clone()))
1750        .collect::<BTreeMap<_, _>>();
1751    let mut edges = Vec::new();
1752
1753    for entry in style_fact_entries {
1754        let mut sass_use_rule_ordinal = 0usize;
1755        let mut sass_forward_rule_ordinal = 0usize;
1756        for edge in &entry.facts.sass_module_edges {
1757            let rule_ordinal = match edge.kind {
1758                "sassUse" => {
1759                    let rule_ordinal = sass_use_rule_ordinal;
1760                    sass_use_rule_ordinal += 1;
1761                    rule_ordinal
1762                }
1763                "sassForward" => {
1764                    let rule_ordinal = sass_forward_rule_ordinal;
1765                    sass_forward_rule_ordinal += 1;
1766                    rule_ordinal
1767                }
1768                _ => 0,
1769            };
1770            let resolution = summarize_omena_resolver_style_module_resolution_with_load_path_roots(
1771                resolver_style_path(entry.style_path.as_str()).as_str(),
1772                edge.source.as_str(),
1773                &resolver_available_style_path_refs,
1774                &resolver_package_manifests,
1775                bundler_path_mappings,
1776                tsconfig_path_mappings,
1777                &load_path_root_refs,
1778            );
1779            let status = if resolution.resolution_kind == "externalIgnored" {
1780                "external"
1781            } else if resolution.resolved_style_path.is_some() {
1782                "resolved"
1783            } else {
1784                "unresolved"
1785            };
1786            let resolved_style_path =
1787                resolution
1788                    .resolved_style_path
1789                    .and_then(|resolved_style_path| {
1790                        canonical_available_style_path(
1791                            resolved_style_path.as_str(),
1792                            &available_style_paths,
1793                        )
1794                        .or(Some(resolved_style_path))
1795                    });
1796            let symlink_chain_link_count = resolution.symlink_chain.link_count;
1797            let symlink_chain_links = resolution
1798                .symlink_chain
1799                .links
1800                .into_iter()
1801                .map(|link| OmenaQuerySymlinkChainLinkV0 {
1802                    link_path: link.link_path,
1803                    target_path: link.target_path,
1804                    target_was_absolute: link.target_was_absolute,
1805                })
1806                .collect::<Vec<_>>();
1807            let configuration_evidence =
1808                transform::derive_static_scss_module_resolution_configuration_evidence(
1809                    entry.style_source.as_str(),
1810                    edge.kind,
1811                    rule_ordinal,
1812                    resolved_style_path.as_deref(),
1813                );
1814            let invalid_configuration_variable_names =
1815                resolved_style_path
1816                    .as_deref()
1817                    .and_then(|target_path| {
1818                        source_by_path.get(target_path).map(|target_source| {
1819                            let configurable_names = transform::derive_static_scss_module_configurable_variable_names_for_resolution(
1820                                target_path,
1821                                target_source,
1822                                &available_style_paths,
1823                                &source_by_path,
1824                                package_manifests,
1825                                bundler_path_mappings,
1826                                tsconfig_path_mappings,
1827                            );
1828                            configuration_evidence
1829                                .configuration_variable_names
1830                                .iter()
1831                                .filter(|name| !configurable_names.contains(*name))
1832                                .cloned()
1833                                .collect::<Vec<_>>()
1834                        })
1835                    })
1836                    .unwrap_or_default();
1837            edges.push(OmenaQuerySassModuleEdgeResolutionV0 {
1838                from_style_path: entry.style_path.clone(),
1839                edge_kind: edge.kind,
1840                source: edge.source.clone(),
1841                rule_ordinal,
1842                namespace_kind: edge.namespace_kind,
1843                namespace: edge.namespace.clone(),
1844                forward_prefix: edge.forward_prefix.clone(),
1845                visibility_filter_kind: edge.visibility_filter_kind,
1846                visibility_filter_names: edge.visibility_filter_names.clone(),
1847                resolved_style_path,
1848                status,
1849                resolution_kind: resolution.resolution_kind,
1850                candidate_count: resolution.candidate_count,
1851                symlink_chain_link_count,
1852                symlink_chain_links,
1853                configuration_signature: configuration_evidence.configuration_signature,
1854                configuration_variable_count: configuration_evidence.configuration_variable_count,
1855                invalid_configuration_variable_names,
1856                module_instance_identity_key: configuration_evidence.module_instance_identity_key,
1857            });
1858        }
1859    }
1860
1861    edges.sort_by_key(|edge| {
1862        (
1863            edge.from_style_path.clone(),
1864            edge.edge_kind,
1865            edge.rule_ordinal,
1866            edge.source.clone(),
1867        )
1868    });
1869    let configurable_names_memo: RefCell<BTreeMap<String, BTreeSet<String>>> =
1870        RefCell::new(BTreeMap::new());
1871    let semantic_edges = sass_module_graph_edge_facts_for_query(&edges);
1872    let semantic_resolution = omena_semantic::summarize_sass_module_graph_resolution(
1873        style_fact_entries.len(),
1874        semantic_edges.as_slice(),
1875        &QuerySassModuleGraphConfigurationResolver {
1876            source_by_path: &source_by_path,
1877            available_style_paths: &available_style_paths,
1878            package_manifests,
1879            bundler_path_mappings,
1880            tsconfig_path_mappings,
1881            configurable_names_memo: &configurable_names_memo,
1882        },
1883    );
1884    let graph_closure_edges = semantic_resolution
1885        .graph_closure_edges
1886        .into_iter()
1887        .map(|edge| OmenaQuerySassModuleGraphClosureEdgeV0 {
1888            from_style_path: edge.from_style_path,
1889            target_style_path: edge.target_style_path,
1890            edge_kind: edge.edge_kind,
1891            depth: edge.depth,
1892            path: edge.path,
1893            namespace_kind: edge.namespace_kind,
1894            namespace: edge.namespace,
1895            forward_prefix: edge.forward_prefix,
1896            visibility_filter_kind: edge.visibility_filter_kind,
1897            visibility_filter_names: edge.visibility_filter_names,
1898            configuration_signature: edge.configuration_signature,
1899            configuration_variable_count: edge.configuration_variable_count,
1900            invalid_configuration_variable_names: edge.invalid_configuration_variable_names,
1901            module_instance_identity_key: edge.module_instance_identity_key,
1902        })
1903        .collect::<Vec<_>>();
1904    let cycles = semantic_resolution
1905        .cycles
1906        .into_iter()
1907        .map(|cycle| OmenaQuerySassModuleCycleV0 { path: cycle.path })
1908        .collect::<Vec<_>>();
1909    let symlink_chain_edge_count = edges
1910        .iter()
1911        .filter(|edge| edge.symlink_chain_link_count > 0)
1912        .count();
1913    let symlink_chain_link_count = edges.iter().map(|edge| edge.symlink_chain_link_count).sum();
1914
1915    OmenaQuerySassModuleCrossFileResolutionV0 {
1916        schema_version: "0",
1917        product: "omena-query.sass-module-cross-file-resolution",
1918        status: "moduleGraphClosureResolved",
1919        resolution_scope: "batchModuleGraph",
1920        style_count: semantic_resolution.style_count,
1921        module_edge_count: semantic_resolution.module_edge_count,
1922        resolved_module_edge_count: semantic_resolution.resolved_module_edge_count,
1923        unresolved_module_edge_count: semantic_resolution.unresolved_module_edge_count,
1924        external_module_edge_count: semantic_resolution.external_module_edge_count,
1925        symlink_chain_edge_count,
1926        symlink_chain_link_count,
1927        configured_module_instance_count: semantic_resolution.configured_module_instance_count,
1928        edges,
1929        graph_closure_edge_count: semantic_resolution.graph_closure_edge_count,
1930        cycle_count: semantic_resolution.cycle_count,
1931        visibility_filter_count: semantic_resolution.visibility_filter_count,
1932        graph_closure_edges,
1933        cycles,
1934        capabilities: OmenaQuerySassModuleCrossFileResolutionCapabilitiesV0 {
1935            omena_parser_module_edge_consumption_ready: true,
1936            resolver_backed_source_resolution_ready: true,
1937            package_manifest_resolution_ready: true,
1938            external_module_filtering_ready: true,
1939            graph_closure_ready: true,
1940            cycle_detection_ready: true,
1941            namespace_show_hide_filter_ready: true,
1942            configured_module_instance_identity_ready: true,
1943            symlink_chain_metadata_ready: true,
1944        },
1945        next_priorities: Vec::new(),
1946    }
1947}
1948
1949fn summarize_sass_module_edge_resolutions_for_module_interface(
1950    projection: &OmenaQueryModuleInterfaceProjectionV0,
1951    available_style_paths: &BTreeSet<&str>,
1952    resolver_available_style_path_refs: &BTreeSet<&str>,
1953    package_manifests: &[OmenaQueryStylePackageManifestV0],
1954    bundler_path_mappings: &[OmenaResolverBundlerPathAliasMappingV0],
1955    tsconfig_path_mappings: &[OmenaResolverTsconfigPathMappingV0],
1956    mut configurable_names_for_target: impl FnMut(&str) -> BTreeSet<String>,
1957) -> Vec<OmenaQuerySassModuleEdgeResolutionV0> {
1958    let load_path_roots = collect_load_path_roots(resolver_available_style_path_refs);
1959    let load_path_root_refs = load_path_roots
1960        .iter()
1961        .map(String::as_str)
1962        .collect::<Vec<_>>();
1963    let resolver_package_manifests = package_manifests
1964        .iter()
1965        .map(|manifest| OmenaResolverStylePackageManifestV0 {
1966            package_json_path: manifest.package_json_path.clone(),
1967            package_json_source: manifest.package_json_source.clone(),
1968        })
1969        .collect::<Vec<_>>();
1970    let mut edges = Vec::new();
1971    let mut sass_use_rule_ordinal = 0usize;
1972    let mut sass_forward_rule_ordinal = 0usize;
1973    for edge in &projection.sass_module_edges {
1974        let rule_ordinal = match edge.kind {
1975            "sassUse" => {
1976                let rule_ordinal = sass_use_rule_ordinal;
1977                sass_use_rule_ordinal += 1;
1978                rule_ordinal
1979            }
1980            "sassForward" => {
1981                let rule_ordinal = sass_forward_rule_ordinal;
1982                sass_forward_rule_ordinal += 1;
1983                rule_ordinal
1984            }
1985            _ => 0,
1986        };
1987        let resolution = summarize_omena_resolver_style_module_resolution_with_load_path_roots(
1988            resolver_style_path(projection.style_path.as_str()).as_str(),
1989            edge.source.as_str(),
1990            resolver_available_style_path_refs,
1991            &resolver_package_manifests,
1992            bundler_path_mappings,
1993            tsconfig_path_mappings,
1994            &load_path_root_refs,
1995        );
1996        let status = if resolution.resolution_kind == "externalIgnored" {
1997            "external"
1998        } else if resolution.resolved_style_path.is_some() {
1999            "resolved"
2000        } else {
2001            "unresolved"
2002        };
2003        let resolved_style_path = resolution
2004            .resolved_style_path
2005            .and_then(|resolved_style_path| {
2006                canonical_available_style_path(resolved_style_path.as_str(), available_style_paths)
2007                    .or(Some(resolved_style_path))
2008            });
2009        let symlink_chain_link_count = resolution.symlink_chain.link_count;
2010        let symlink_chain_links = resolution
2011            .symlink_chain
2012            .links
2013            .into_iter()
2014            .map(|link| OmenaQuerySymlinkChainLinkV0 {
2015                link_path: link.link_path,
2016                target_path: link.target_path,
2017                target_was_absolute: link.target_was_absolute,
2018            })
2019            .collect::<Vec<_>>();
2020        let variable_overrides =
2021            sass_module_rule_variable_overrides_from_interface(projection, edge.kind, rule_ordinal);
2022        let invalid_configuration_variable_names = resolved_style_path
2023            .as_deref()
2024            .filter(|_| !variable_overrides.is_empty())
2025            .map(|target_path| {
2026                let configurable_names = configurable_names_for_target(target_path);
2027                variable_overrides
2028                    .keys()
2029                    .filter(|name| !configurable_names.contains(*name))
2030                    .cloned()
2031                    .collect::<Vec<_>>()
2032            })
2033            .unwrap_or_default();
2034        let module_instance_identity_key = match edge.kind {
2035            "sassUse" | "sassForward" => resolved_style_path.as_deref().map(|target_path| {
2036                omena_semantic::summarize_sass_module_instance_identity_key(
2037                    target_path,
2038                    &variable_overrides,
2039                )
2040            }),
2041            _ => None,
2042        };
2043        edges.push(OmenaQuerySassModuleEdgeResolutionV0 {
2044            from_style_path: projection.style_path.clone(),
2045            edge_kind: edge.kind,
2046            source: edge.source.clone(),
2047            rule_ordinal,
2048            namespace_kind: edge.namespace_kind,
2049            namespace: edge.namespace.clone(),
2050            forward_prefix: edge.forward_prefix.clone(),
2051            visibility_filter_kind: edge.visibility_filter_kind,
2052            visibility_filter_names: edge.visibility_filter_names.clone(),
2053            resolved_style_path,
2054            status,
2055            resolution_kind: resolution.resolution_kind,
2056            candidate_count: resolution.candidate_count,
2057            symlink_chain_link_count,
2058            symlink_chain_links,
2059            configuration_signature: omena_semantic::summarize_sass_module_configuration_signature(
2060                &variable_overrides,
2061            ),
2062            configuration_variable_count: variable_overrides.len(),
2063            invalid_configuration_variable_names,
2064            module_instance_identity_key,
2065        });
2066    }
2067    edges.sort_by_key(|edge| {
2068        (
2069            edge.from_style_path.clone(),
2070            edge.edge_kind,
2071            edge.rule_ordinal,
2072            edge.source.clone(),
2073        )
2074    });
2075    edges
2076}
2077
2078fn summarize_sass_module_cross_file_resolution_from_module_interfaces_and_edges(
2079    module_interfaces: &[OmenaQueryModuleInterfaceProjectionV0],
2080    edges: Vec<OmenaQuerySassModuleEdgeResolutionV0>,
2081    configurable_names_by_path: &BTreeMap<String, BTreeSet<String>>,
2082) -> OmenaQuerySassModuleCrossFileResolutionV0 {
2083    let module_interface_by_path = module_interfaces
2084        .iter()
2085        .map(|projection| (projection.style_path.clone(), projection))
2086        .collect::<BTreeMap<_, _>>();
2087    let semantic_edges = sass_module_graph_edge_facts_for_query(&edges);
2088    let semantic_resolution = omena_semantic::summarize_sass_module_graph_resolution(
2089        module_interfaces.len(),
2090        semantic_edges.as_slice(),
2091        &ModuleInterfaceSassModuleGraphConfigurationResolver {
2092            module_interface_by_path: &module_interface_by_path,
2093            configurable_names_by_path,
2094        },
2095    );
2096    let graph_closure_edges = semantic_resolution
2097        .graph_closure_edges
2098        .into_iter()
2099        .map(|edge| OmenaQuerySassModuleGraphClosureEdgeV0 {
2100            from_style_path: edge.from_style_path,
2101            target_style_path: edge.target_style_path,
2102            edge_kind: edge.edge_kind,
2103            depth: edge.depth,
2104            path: edge.path,
2105            namespace_kind: edge.namespace_kind,
2106            namespace: edge.namespace,
2107            forward_prefix: edge.forward_prefix,
2108            visibility_filter_kind: edge.visibility_filter_kind,
2109            visibility_filter_names: edge.visibility_filter_names,
2110            configuration_signature: edge.configuration_signature,
2111            configuration_variable_count: edge.configuration_variable_count,
2112            invalid_configuration_variable_names: edge.invalid_configuration_variable_names,
2113            module_instance_identity_key: edge.module_instance_identity_key,
2114        })
2115        .collect::<Vec<_>>();
2116    let cycles = semantic_resolution
2117        .cycles
2118        .into_iter()
2119        .map(|cycle| OmenaQuerySassModuleCycleV0 { path: cycle.path })
2120        .collect::<Vec<_>>();
2121    let symlink_chain_edge_count = edges
2122        .iter()
2123        .filter(|edge| edge.symlink_chain_link_count > 0)
2124        .count();
2125    let symlink_chain_link_count = edges.iter().map(|edge| edge.symlink_chain_link_count).sum();
2126
2127    OmenaQuerySassModuleCrossFileResolutionV0 {
2128        schema_version: "0",
2129        product: "omena-query.sass-module-cross-file-resolution",
2130        status: "moduleGraphClosureResolved",
2131        resolution_scope: "batchModuleGraph",
2132        style_count: semantic_resolution.style_count,
2133        module_edge_count: semantic_resolution.module_edge_count,
2134        resolved_module_edge_count: semantic_resolution.resolved_module_edge_count,
2135        unresolved_module_edge_count: semantic_resolution.unresolved_module_edge_count,
2136        external_module_edge_count: semantic_resolution.external_module_edge_count,
2137        symlink_chain_edge_count,
2138        symlink_chain_link_count,
2139        configured_module_instance_count: semantic_resolution.configured_module_instance_count,
2140        edges,
2141        graph_closure_edge_count: semantic_resolution.graph_closure_edge_count,
2142        cycle_count: semantic_resolution.cycle_count,
2143        visibility_filter_count: semantic_resolution.visibility_filter_count,
2144        graph_closure_edges,
2145        cycles,
2146        capabilities: OmenaQuerySassModuleCrossFileResolutionCapabilitiesV0 {
2147            omena_parser_module_edge_consumption_ready: true,
2148            resolver_backed_source_resolution_ready: true,
2149            package_manifest_resolution_ready: true,
2150            external_module_filtering_ready: true,
2151            graph_closure_ready: true,
2152            cycle_detection_ready: true,
2153            namespace_show_hide_filter_ready: true,
2154            configured_module_instance_identity_ready: true,
2155            symlink_chain_metadata_ready: true,
2156        },
2157        next_priorities: Vec::new(),
2158    }
2159}
2160
2161fn canonical_available_style_path(
2162    candidate: &str,
2163    available_style_paths: &BTreeSet<&str>,
2164) -> Option<String> {
2165    if available_style_paths.contains(candidate) {
2166        return Some(candidate.to_string());
2167    }
2168    let candidate_path = style_path_equivalence_key(candidate)?;
2169    available_style_paths
2170        .iter()
2171        .find(|available| {
2172            style_path_equivalence_key(available).as_deref() == Some(candidate_path.as_path())
2173        })
2174        .map(|available| (*available).to_string())
2175}
2176
2177fn sass_module_rule_variable_overrides_from_interface(
2178    projection: &OmenaQueryModuleInterfaceProjectionV0,
2179    edge_kind: &'static str,
2180    rule_ordinal: usize,
2181) -> BTreeMap<String, String> {
2182    projection
2183        .sass_module_rule_configurations
2184        .iter()
2185        .find(|surface| surface.edge_kind == edge_kind && surface.rule_ordinal == rule_ordinal)
2186        .map(|surface| surface.variable_overrides.clone())
2187        .unwrap_or_default()
2188}
2189
2190fn sass_module_forward_variable_overrides_from_interface(
2191    projection: &OmenaQueryModuleInterfaceProjectionV0,
2192    rule_ordinal: usize,
2193) -> BTreeMap<String, omena_semantic::SassModuleVariableOverrideV0> {
2194    sass_module_forward_variable_overrides_from_rule_configurations(
2195        projection.sass_module_rule_configurations.as_slice(),
2196        rule_ordinal,
2197    )
2198}
2199
2200fn sass_module_forward_variable_overrides_from_rule_configurations(
2201    rule_configurations: &[OmenaQuerySassModuleRuleConfigurationSurfaceV0],
2202    rule_ordinal: usize,
2203) -> BTreeMap<String, omena_semantic::SassModuleVariableOverrideV0> {
2204    rule_configurations
2205        .iter()
2206        .find(|surface| surface.edge_kind == "sassForward" && surface.rule_ordinal == rule_ordinal)
2207        .map(|surface| surface.forward_variable_overrides.clone())
2208        .unwrap_or_default()
2209}
2210
2211#[derive(Debug, Clone, Copy)]
2212struct ModuleInterfaceSassModuleGraphConfigurationResolver<'a> {
2213    module_interface_by_path: &'a BTreeMap<String, &'a OmenaQueryModuleInterfaceProjectionV0>,
2214    configurable_names_by_path: &'a BTreeMap<String, BTreeSet<String>>,
2215}
2216
2217impl omena_semantic::SassModuleGraphConfigurationResolverV0
2218    for ModuleInterfaceSassModuleGraphConfigurationResolver<'_>
2219{
2220    fn use_variable_overrides(
2221        &self,
2222        request: omena_semantic::SassModuleUseConfigurationRequestV0<'_>,
2223    ) -> BTreeMap<String, String> {
2224        self.module_interface_by_path
2225            .get(request.from_style_path)
2226            .map(|projection| {
2227                sass_module_rule_variable_overrides_from_interface(
2228                    projection,
2229                    "sassUse",
2230                    request.rule_ordinal,
2231                )
2232            })
2233            .unwrap_or_default()
2234    }
2235
2236    fn forward_effective_variable_overrides(
2237        &self,
2238        request: omena_semantic::SassModuleForwardConfigurationRequestV0<'_>,
2239    ) -> BTreeMap<String, String> {
2240        let Some(projection) = self.module_interface_by_path.get(request.from_style_path) else {
2241            return BTreeMap::new();
2242        };
2243        let explicit_variable_overrides =
2244            sass_module_forward_variable_overrides_from_interface(projection, request.rule_ordinal);
2245        omena_semantic::derive_sass_forward_effective_variable_overrides(
2246            &explicit_variable_overrides,
2247            request.inherited_variable_overrides,
2248            request.forward_prefix,
2249            request.visibility_filter_kind,
2250            request.visibility_filter_names,
2251            request.configurable_names,
2252        )
2253    }
2254
2255    fn configurable_names(&self, target_style_path: &str) -> BTreeSet<String> {
2256        self.configurable_names_by_path
2257            .get(target_style_path)
2258            .cloned()
2259            .unwrap_or_default()
2260    }
2261}
2262
2263fn style_path_equivalence_key(path_or_uri: &str) -> Option<PathBuf> {
2264    let path = path_or_uri.strip_prefix("file://").unwrap_or(path_or_uri);
2265    Some(Path::new(path).components().collect())
2266}
2267
2268fn resolver_style_path(path_or_uri: &str) -> String {
2269    path_or_uri
2270        .strip_prefix("file://")
2271        .unwrap_or(path_or_uri)
2272        .to_string()
2273}
2274
2275#[derive(Debug, Clone, Copy)]
2276struct QuerySassModuleGraphConfigurationResolver<'a> {
2277    source_by_path: &'a BTreeMap<String, String>,
2278    available_style_paths: &'a BTreeSet<&'a str>,
2279    package_manifests: &'a [OmenaQueryStylePackageManifestV0],
2280    bundler_path_mappings: &'a [OmenaResolverBundlerPathAliasMappingV0],
2281    tsconfig_path_mappings: &'a [OmenaResolverTsconfigPathMappingV0],
2282    configurable_names_memo: &'a RefCell<BTreeMap<String, BTreeSet<String>>>,
2283}
2284
2285impl omena_semantic::SassModuleGraphConfigurationResolverV0
2286    for QuerySassModuleGraphConfigurationResolver<'_>
2287{
2288    fn use_variable_overrides(
2289        &self,
2290        request: omena_semantic::SassModuleUseConfigurationRequestV0<'_>,
2291    ) -> BTreeMap<String, String> {
2292        let Some(style_source) = self.source_by_path.get(request.from_style_path) else {
2293            return BTreeMap::new();
2294        };
2295        omena_semantic::derive_sass_module_rule_variable_overrides_at_ordinal(
2296            style_source,
2297            "@use",
2298            request.rule_ordinal,
2299        )
2300    }
2301
2302    fn forward_effective_variable_overrides(
2303        &self,
2304        request: omena_semantic::SassModuleForwardConfigurationRequestV0<'_>,
2305    ) -> BTreeMap<String, String> {
2306        let Some(style_source) = self.source_by_path.get(request.from_style_path) else {
2307            return BTreeMap::new();
2308        };
2309        omena_semantic::derive_sass_module_forward_effective_variable_overrides_at_ordinal(
2310            style_source,
2311            request.rule_ordinal,
2312            request.inherited_variable_overrides,
2313            request.forward_prefix,
2314            request.visibility_filter_kind,
2315            request.visibility_filter_names,
2316            request.configurable_names,
2317        )
2318    }
2319
2320    fn configurable_names(&self, target_style_path: &str) -> BTreeSet<String> {
2321        memoized_configurable_names(target_style_path, self)
2322    }
2323}
2324
2325fn sass_module_graph_edge_facts_for_query(
2326    edges: &[OmenaQuerySassModuleEdgeResolutionV0],
2327) -> Vec<omena_semantic::SassModuleGraphEdgeFactV0> {
2328    edges
2329        .iter()
2330        .map(|edge| omena_semantic::SassModuleGraphEdgeFactV0 {
2331            from_style_path: edge.from_style_path.clone(),
2332            edge_kind: edge.edge_kind,
2333            source: edge.source.clone(),
2334            rule_ordinal: edge.rule_ordinal,
2335            namespace_kind: edge.namespace_kind,
2336            namespace: edge.namespace.clone(),
2337            forward_prefix: edge.forward_prefix.clone(),
2338            visibility_filter_kind: edge.visibility_filter_kind,
2339            visibility_filter_names: edge.visibility_filter_names.clone(),
2340            resolved_style_path: edge.resolved_style_path.clone(),
2341            status: edge.status,
2342            configuration_signature: edge.configuration_signature.clone(),
2343            configuration_variable_count: edge.configuration_variable_count,
2344            invalid_configuration_variable_names: edge.invalid_configuration_variable_names.clone(),
2345            module_instance_identity_key: edge.module_instance_identity_key.clone(),
2346        })
2347        .collect()
2348}
2349
2350// Test-only counter of ACTUAL configurable-name derivations (memo misses that run the parse +
2351// disk-resolution work). With the L1 memo this is O(distinct modules); without it the same
2352// derivation runs per enumerated closure path = O(paths) (super-polynomial). The end-to-end
2353// growth gate (tests) asserts this stays ~linear, catching a regression of the L1 memo that the
2354// output-only equivalence oracle cannot see. Compiled out of non-test builds (zero overhead).
2355#[cfg(test)]
2356thread_local! {
2357    static CONFIGURABLE_NAMES_DERIVATIONS: std::cell::Cell<u64> = const { std::cell::Cell::new(0) };
2358}
2359
2360#[cfg(test)]
2361pub(crate) fn reset_configurable_names_derivation_count() {
2362    CONFIGURABLE_NAMES_DERIVATIONS.with(|count| count.set(0));
2363}
2364
2365#[cfg(test)]
2366pub(crate) fn configurable_names_derivation_count() -> u64 {
2367    CONFIGURABLE_NAMES_DERIVATIONS.with(|count| count.get())
2368}
2369
2370#[cfg(test)]
2371pub(crate) fn with_rawallpaths_closure<R>(body: impl FnOnce() -> R) -> R {
2372    omena_semantic::with_sass_module_rawallpaths_closure_for_test(body)
2373}
2374
2375fn memoized_configurable_names(
2376    target_style_path: &str,
2377    context: &QuerySassModuleGraphConfigurationResolver<'_>,
2378) -> BTreeSet<String> {
2379    {
2380        let cache = context.configurable_names_memo.borrow();
2381        if let Some(cached) = cache.get(target_style_path) {
2382            return cached.clone();
2383        }
2384    }
2385    let computed = context
2386        .source_by_path
2387        .get(target_style_path)
2388        .map(|target_source| {
2389            #[cfg(test)]
2390            CONFIGURABLE_NAMES_DERIVATIONS.with(|count| count.set(count.get() + 1));
2391            transform::derive_static_scss_module_configurable_variable_names_for_resolution(
2392                target_style_path,
2393                target_source,
2394                context.available_style_paths,
2395                context.source_by_path,
2396                context.package_manifests,
2397                context.bundler_path_mappings,
2398                context.tsconfig_path_mappings,
2399            )
2400        })
2401        .unwrap_or_default();
2402    context
2403        .configurable_names_memo
2404        .borrow_mut()
2405        .insert(target_style_path.to_string(), computed.clone());
2406    computed
2407}
2408
2409fn summarize_css_modules_cross_file_resolution(
2410    style_fact_entries: &[OmenaQueryStyleFactEntry],
2411    package_manifests: &[OmenaQueryStylePackageManifestV0],
2412) -> OmenaQueryCssModulesCrossFileResolutionV0 {
2413    summarize_css_modules_cross_file_resolution_with_resolution_inputs(
2414        style_fact_entries,
2415        package_manifests,
2416        &OmenaQueryStyleResolutionInputsV0::default(),
2417    )
2418}
2419
2420fn summarize_css_modules_cross_file_resolution_with_resolution_inputs(
2421    style_fact_entries: &[OmenaQueryStyleFactEntry],
2422    package_manifests: &[OmenaQueryStylePackageManifestV0],
2423    resolution_inputs: &OmenaQueryStyleResolutionInputsV0,
2424) -> OmenaQueryCssModulesCrossFileResolutionV0 {
2425    let semantic_facts = css_modules_cross_file_style_facts_for_query(style_fact_entries);
2426    let style_import_edges = style_import_reachability_edges_for_query(
2427        style_fact_entries,
2428        package_manifests,
2429        resolution_inputs,
2430    );
2431    summarize_css_modules_cross_file_resolution_from_semantic_inputs(
2432        semantic_facts.as_slice(),
2433        style_import_edges.as_slice(),
2434        package_manifests,
2435    )
2436}
2437
2438fn summarize_css_modules_cross_file_resolution_from_module_interfaces_and_import_edges(
2439    module_interfaces: &[OmenaQueryModuleInterfaceProjectionV0],
2440    package_manifests: &[OmenaQueryStylePackageManifestV0],
2441    edges: Vec<OmenaQueryCssModulesImportEdgeResolutionV0>,
2442) -> OmenaQueryCssModulesCrossFileResolutionV0 {
2443    let semantic_facts = module_interfaces
2444        .iter()
2445        .map(|projection| projection.css_modules_style_facts.clone())
2446        .collect::<Vec<_>>();
2447    let semantic_package_manifests = semantic_package_manifests_for_query(package_manifests);
2448    let closure_summary = omena_semantic::summarize_css_modules_cross_file_closure(
2449        semantic_facts.as_slice(),
2450        semantic_package_manifests.as_slice(),
2451    );
2452    let composes_closure_edges = closure_summary
2453        .composes_closure_edges
2454        .into_iter()
2455        .map(|edge| OmenaQueryCssModulesComposesClosureEdgeV0 {
2456            from_style_path: edge.from_style_path,
2457            owner_selector_name: edge.owner_selector_name,
2458            target_style_path: edge.target_style_path,
2459            target_selector_name: edge.target_selector_name,
2460            depth: edge.depth,
2461            path: edge.path,
2462        })
2463        .collect::<Vec<_>>();
2464    let value_closure_edges = closure_summary
2465        .value_closure_edges
2466        .into_iter()
2467        .map(|edge| OmenaQueryCssModulesValueClosureEdgeV0 {
2468            from_style_path: edge.from_style_path,
2469            value_name: edge.value_name,
2470            target_style_path: edge.target_style_path,
2471            target_value_name: edge.target_value_name,
2472            depth: edge.depth,
2473            path: edge.path,
2474        })
2475        .collect::<Vec<_>>();
2476    let icss_closure_edges = closure_summary
2477        .icss_closure_edges
2478        .into_iter()
2479        .map(|edge| OmenaQueryCssModulesIcssClosureEdgeV0 {
2480            from_style_path: edge.from_style_path,
2481            name: edge.name,
2482            target_style_path: edge.target_style_path,
2483            target_name: edge.target_name,
2484            depth: edge.depth,
2485            path: edge.path,
2486        })
2487        .collect::<Vec<_>>();
2488    let cycles = closure_summary
2489        .cycles
2490        .into_iter()
2491        .map(|cycle| OmenaQueryCssModulesCycleV0 {
2492            kind: cycle.kind,
2493            path: cycle.path,
2494        })
2495        .collect::<Vec<_>>();
2496
2497    css_modules_cross_file_resolution_from_query_parts(
2498        semantic_facts.len(),
2499        edges,
2500        OmenaQueryCssModulesClosurePartsV0 {
2501            composes_closure_edge_count: closure_summary.composes_closure_edge_count,
2502            value_closure_edge_count: closure_summary.value_closure_edge_count,
2503            icss_closure_edge_count: closure_summary.icss_closure_edge_count,
2504            composes_cycle_count: closure_summary.composes_cycle_count,
2505            value_cycle_count: closure_summary.value_cycle_count,
2506            icss_cycle_count: closure_summary.icss_cycle_count,
2507            composes_closure_edges,
2508            value_closure_edges,
2509            icss_closure_edges,
2510            cycles,
2511        },
2512    )
2513}
2514
2515fn summarize_css_modules_import_edge_resolutions_for_module_interface(
2516    origin: &OmenaQueryModuleInterfaceProjectionV0,
2517    target_interfaces: &[OmenaQueryModuleInterfaceProjectionV0],
2518    available_style_paths: &BTreeSet<&str>,
2519    style_import_edges: &[omena_semantic::StyleImportReachabilityEdgeFactV0],
2520    package_manifests: &[OmenaQueryStylePackageManifestV0],
2521    resolution_inputs: &OmenaQueryStyleResolutionInputsV0,
2522) -> Vec<OmenaQueryCssModulesImportEdgeResolutionV0> {
2523    let mut facts_by_path = target_interfaces
2524        .iter()
2525        .map(|projection| {
2526            (
2527                projection.style_path.as_str(),
2528                &projection.css_modules_style_facts,
2529            )
2530        })
2531        .collect::<BTreeMap<_, _>>();
2532    facts_by_path.insert(origin.style_path.as_str(), &origin.css_modules_style_facts);
2533    let reachable =
2534        css_modules_import_reachability_for_origin(origin.style_path.as_str(), style_import_edges);
2535    let mut edges = Vec::new();
2536
2537    for edge in &origin.css_modules_style_facts.css_module_composes_edges {
2538        let Some(source) = edge.import_source.as_deref() else {
2539            continue;
2540        };
2541        edges.push(resolve_css_modules_import_edge_for_query(
2542            origin.style_path.as_str(),
2543            "composes",
2544            source,
2545            edge.target_names.as_slice(),
2546            available_style_paths,
2547            &facts_by_path,
2548            &reachable,
2549            package_manifests,
2550            resolution_inputs,
2551            |target| target.class_selector_names.as_slice(),
2552        ));
2553    }
2554
2555    for edge in &origin.css_modules_style_facts.css_module_value_import_edges {
2556        edges.push(resolve_css_modules_import_edge_for_query(
2557            origin.style_path.as_str(),
2558            "value",
2559            edge.import_source.as_str(),
2560            std::slice::from_ref(&edge.remote_name),
2561            available_style_paths,
2562            &facts_by_path,
2563            &reachable,
2564            package_manifests,
2565            resolution_inputs,
2566            |target| target.css_module_value_definition_names.as_slice(),
2567        ));
2568    }
2569
2570    for edge in &origin.css_modules_style_facts.icss_import_edges {
2571        edges.push(resolve_css_modules_import_edge_for_query(
2572            origin.style_path.as_str(),
2573            "icss",
2574            edge.import_source.as_str(),
2575            std::slice::from_ref(&edge.remote_name),
2576            available_style_paths,
2577            &facts_by_path,
2578            &reachable,
2579            package_manifests,
2580            resolution_inputs,
2581            |target| target.icss_export_names.as_slice(),
2582        ));
2583    }
2584
2585    edges.sort_by_key(|edge| {
2586        (
2587            edge.from_style_path.clone(),
2588            edge.import_kind,
2589            edge.source.clone(),
2590        )
2591    });
2592    edges
2593}
2594
2595fn summarize_css_modules_cross_file_resolution_from_semantic_inputs(
2596    semantic_facts: &[omena_semantic::CssModulesCrossFileStyleFactsV0],
2597    style_import_edges: &[omena_semantic::StyleImportReachabilityEdgeFactV0],
2598    package_manifests: &[OmenaQueryStylePackageManifestV0],
2599) -> OmenaQueryCssModulesCrossFileResolutionV0 {
2600    let semantic_package_manifests = semantic_package_manifests_for_query(package_manifests);
2601    let semantic_resolution = omena_semantic::summarize_css_modules_cross_file_resolution(
2602        semantic_facts,
2603        style_import_edges,
2604        semantic_package_manifests.as_slice(),
2605    );
2606    let composes_closure_edges = semantic_resolution
2607        .composes_closure_edges
2608        .into_iter()
2609        .map(|edge| OmenaQueryCssModulesComposesClosureEdgeV0 {
2610            from_style_path: edge.from_style_path,
2611            owner_selector_name: edge.owner_selector_name,
2612            target_style_path: edge.target_style_path,
2613            target_selector_name: edge.target_selector_name,
2614            depth: edge.depth,
2615            path: edge.path,
2616        })
2617        .collect::<Vec<_>>();
2618    let value_closure_edges = semantic_resolution
2619        .value_closure_edges
2620        .into_iter()
2621        .map(|edge| OmenaQueryCssModulesValueClosureEdgeV0 {
2622            from_style_path: edge.from_style_path,
2623            value_name: edge.value_name,
2624            target_style_path: edge.target_style_path,
2625            target_value_name: edge.target_value_name,
2626            depth: edge.depth,
2627            path: edge.path,
2628        })
2629        .collect::<Vec<_>>();
2630    let icss_closure_edges = semantic_resolution
2631        .icss_closure_edges
2632        .into_iter()
2633        .map(|edge| OmenaQueryCssModulesIcssClosureEdgeV0 {
2634            from_style_path: edge.from_style_path,
2635            name: edge.name,
2636            target_style_path: edge.target_style_path,
2637            target_name: edge.target_name,
2638            depth: edge.depth,
2639            path: edge.path,
2640        })
2641        .collect::<Vec<_>>();
2642    let edges = semantic_resolution
2643        .edges
2644        .into_iter()
2645        .map(|edge| OmenaQueryCssModulesImportEdgeResolutionV0 {
2646            from_style_path: edge.from_style_path,
2647            import_kind: edge.import_kind,
2648            source: edge.source,
2649            resolved_style_path: edge.resolved_style_path,
2650            status: edge.status,
2651            import_graph_distance: edge.import_graph_distance,
2652            import_graph_order: edge.import_graph_order,
2653            imported_names: edge.imported_names,
2654            exported_names: edge.exported_names,
2655            matched_names: edge.matched_names,
2656        })
2657        .collect::<Vec<_>>();
2658    let cycles = semantic_resolution
2659        .cycles
2660        .into_iter()
2661        .map(|cycle| OmenaQueryCssModulesCycleV0 {
2662            kind: cycle.kind,
2663            path: cycle.path,
2664        })
2665        .collect::<Vec<_>>();
2666
2667    OmenaQueryCssModulesCrossFileResolutionV0 {
2668        schema_version: "0",
2669        product: "omena-query.css-modules-cross-file-resolution",
2670        status: "semanticLayerOwnedResolutionAdapter",
2671        resolution_scope: "batchImportGraph",
2672        style_count: semantic_resolution.style_count,
2673        import_edge_count: semantic_resolution.import_edge_count,
2674        resolved_import_edge_count: semantic_resolution.resolved_import_edge_count,
2675        unresolved_import_edge_count: semantic_resolution.unresolved_import_edge_count,
2676        matched_name_count: semantic_resolution.matched_name_count,
2677        edges,
2678        composes_closure_edge_count: composes_closure_edges.len(),
2679        value_closure_edge_count: value_closure_edges.len(),
2680        icss_closure_edge_count: icss_closure_edges.len(),
2681        composes_cycle_count: semantic_resolution.composes_cycle_count,
2682        value_cycle_count: semantic_resolution.value_cycle_count,
2683        icss_cycle_count: semantic_resolution.icss_cycle_count,
2684        composes_closure_edges,
2685        value_closure_edges,
2686        icss_closure_edges,
2687        cycles,
2688        capabilities: OmenaQueryCssModulesCrossFileResolutionCapabilitiesV0 {
2689            semantic_layer_owned: semantic_resolution.capabilities.semantic_layer_owned,
2690            import_source_resolution_ready: semantic_resolution
2691                .capabilities
2692                .import_source_resolution_ready,
2693            cross_file_resolution_ready: true,
2694            composes_closure_ready: semantic_resolution.capabilities.transitive_closure_ready,
2695            composes_name_match_ready: semantic_resolution.capabilities.composes_name_match_ready,
2696            value_name_match_ready: semantic_resolution.capabilities.value_name_match_ready,
2697            icss_name_match_ready: semantic_resolution.capabilities.icss_name_match_ready,
2698            transitive_closure_ready: semantic_resolution.capabilities.transitive_closure_ready,
2699            value_graph_closure_ready: semantic_resolution.capabilities.value_graph_closure_ready,
2700            icss_export_import_closure_ready: semantic_resolution
2701                .capabilities
2702                .icss_export_import_closure_ready,
2703            cycle_detection_ready: semantic_resolution.capabilities.cycle_detection_ready,
2704        },
2705        next_priorities: vec![],
2706    }
2707}
2708
2709#[derive(Debug, Clone, PartialEq, Eq)]
2710struct OmenaQueryCssModulesClosurePartsV0 {
2711    composes_closure_edge_count: usize,
2712    value_closure_edge_count: usize,
2713    icss_closure_edge_count: usize,
2714    composes_cycle_count: usize,
2715    value_cycle_count: usize,
2716    icss_cycle_count: usize,
2717    composes_closure_edges: Vec<OmenaQueryCssModulesComposesClosureEdgeV0>,
2718    value_closure_edges: Vec<OmenaQueryCssModulesValueClosureEdgeV0>,
2719    icss_closure_edges: Vec<OmenaQueryCssModulesIcssClosureEdgeV0>,
2720    cycles: Vec<OmenaQueryCssModulesCycleV0>,
2721}
2722
2723fn css_modules_cross_file_resolution_from_query_parts(
2724    style_count: usize,
2725    edges: Vec<OmenaQueryCssModulesImportEdgeResolutionV0>,
2726    closure: OmenaQueryCssModulesClosurePartsV0,
2727) -> OmenaQueryCssModulesCrossFileResolutionV0 {
2728    let resolved_import_edge_count = edges
2729        .iter()
2730        .filter(|edge| edge.resolved_style_path.is_some())
2731        .count();
2732    let matched_name_count = edges
2733        .iter()
2734        .map(|edge| edge.matched_names.len())
2735        .sum::<usize>();
2736
2737    OmenaQueryCssModulesCrossFileResolutionV0 {
2738        schema_version: "0",
2739        product: "omena-query.css-modules-cross-file-resolution",
2740        status: "semanticLayerOwnedResolutionAdapter",
2741        resolution_scope: "batchImportGraph",
2742        style_count,
2743        import_edge_count: edges.len(),
2744        resolved_import_edge_count,
2745        unresolved_import_edge_count: edges.len() - resolved_import_edge_count,
2746        matched_name_count,
2747        edges,
2748        composes_closure_edge_count: closure.composes_closure_edge_count,
2749        value_closure_edge_count: closure.value_closure_edge_count,
2750        icss_closure_edge_count: closure.icss_closure_edge_count,
2751        composes_cycle_count: closure.composes_cycle_count,
2752        value_cycle_count: closure.value_cycle_count,
2753        icss_cycle_count: closure.icss_cycle_count,
2754        composes_closure_edges: closure.composes_closure_edges,
2755        value_closure_edges: closure.value_closure_edges,
2756        icss_closure_edges: closure.icss_closure_edges,
2757        cycles: closure.cycles,
2758        capabilities: OmenaQueryCssModulesCrossFileResolutionCapabilitiesV0 {
2759            semantic_layer_owned: true,
2760            import_source_resolution_ready: true,
2761            cross_file_resolution_ready: true,
2762            composes_closure_ready: true,
2763            composes_name_match_ready: true,
2764            value_name_match_ready: true,
2765            icss_name_match_ready: true,
2766            transitive_closure_ready: true,
2767            value_graph_closure_ready: true,
2768            icss_export_import_closure_ready: true,
2769            cycle_detection_ready: true,
2770        },
2771        next_priorities: vec![],
2772    }
2773}
2774
2775#[derive(Debug, Clone, Copy, PartialEq, Eq)]
2776struct CssModulesImportReachabilityForQuery {
2777    distance: usize,
2778    order: usize,
2779}
2780
2781fn css_modules_import_reachability_for_origin(
2782    origin_style_path: &str,
2783    style_import_edges: &[omena_semantic::StyleImportReachabilityEdgeFactV0],
2784) -> BTreeMap<String, CssModulesImportReachabilityForQuery> {
2785    omena_semantic::summarize_style_import_reachability(origin_style_path, style_import_edges)
2786        .reachable_style_paths
2787        .into_iter()
2788        .map(|fact| {
2789            (
2790                fact.style_path,
2791                CssModulesImportReachabilityForQuery {
2792                    distance: fact.distance,
2793                    order: fact.order,
2794                },
2795            )
2796        })
2797        .collect()
2798}
2799
2800#[allow(clippy::too_many_arguments)]
2801fn resolve_css_modules_import_edge_for_query(
2802    from_style_path: &str,
2803    import_kind: &'static str,
2804    source: &str,
2805    imported_names: &[String],
2806    available_style_paths: &BTreeSet<&str>,
2807    facts_by_path: &BTreeMap<&str, &omena_semantic::CssModulesCrossFileStyleFactsV0>,
2808    reachable: &BTreeMap<String, CssModulesImportReachabilityForQuery>,
2809    package_manifests: &[OmenaQueryStylePackageManifestV0],
2810    resolution_inputs: &OmenaQueryStyleResolutionInputsV0,
2811    exported_names_for_kind: fn(&omena_semantic::CssModulesCrossFileStyleFactsV0) -> &[String],
2812) -> OmenaQueryCssModulesImportEdgeResolutionV0 {
2813    let resolved_style_path = resolve_style_module_source_with_resolution_inputs(
2814        from_style_path,
2815        source,
2816        available_style_paths,
2817        package_manifests,
2818        resolution_inputs,
2819    );
2820    let reachability = resolved_style_path
2821        .as_ref()
2822        .and_then(|style_path| reachable.get(style_path));
2823    let exported_names = resolved_style_path
2824        .as_deref()
2825        .and_then(|style_path| facts_by_path.get(style_path))
2826        .map(|facts| exported_names_for_kind(facts).to_vec())
2827        .unwrap_or_default();
2828    let imported_names = sorted_unique_query_strings(imported_names);
2829    let matched_names =
2830        sorted_query_name_intersection(imported_names.as_slice(), exported_names.as_slice());
2831    let status = if resolved_style_path.is_none() {
2832        "unresolvedSource"
2833    } else if imported_names.is_empty() {
2834        "resolvedSource"
2835    } else if matched_names.is_empty() {
2836        "resolvedSourceNoNameMatch"
2837    } else {
2838        "resolved"
2839    };
2840
2841    OmenaQueryCssModulesImportEdgeResolutionV0 {
2842        from_style_path: from_style_path.to_string(),
2843        import_kind,
2844        source: source.to_string(),
2845        resolved_style_path,
2846        status,
2847        import_graph_distance: reachability.map(|reachability| reachability.distance),
2848        import_graph_order: reachability.map(|reachability| reachability.order),
2849        imported_names,
2850        exported_names,
2851        matched_names,
2852    }
2853}
2854
2855fn sorted_unique_query_strings(values: &[String]) -> Vec<String> {
2856    values
2857        .iter()
2858        .cloned()
2859        .collect::<BTreeSet<_>>()
2860        .into_iter()
2861        .collect()
2862}
2863
2864fn sorted_query_name_intersection(left: &[String], right: &[String]) -> Vec<String> {
2865    let right = right.iter().map(String::as_str).collect::<BTreeSet<_>>();
2866    left.iter()
2867        .filter(|name| right.contains(name.as_str()))
2868        .cloned()
2869        .collect::<BTreeSet<_>>()
2870        .into_iter()
2871        .collect()
2872}
2873
2874fn css_modules_cross_file_style_facts_for_query(
2875    style_fact_entries: &[OmenaQueryStyleFactEntry],
2876) -> Vec<omena_semantic::CssModulesCrossFileStyleFactsV0> {
2877    style_fact_entries
2878        .iter()
2879        .map(css_modules_cross_file_style_fact_for_query)
2880        .collect()
2881}
2882
2883fn css_modules_cross_file_style_fact_for_query(
2884    entry: &OmenaQueryStyleFactEntry,
2885) -> omena_semantic::CssModulesCrossFileStyleFactsV0 {
2886    omena_semantic::CssModulesCrossFileStyleFactsV0 {
2887        style_path: entry.style_path.clone(),
2888        class_selector_names: entry.facts.class_selector_names.clone(),
2889        css_module_value_definition_names: entry.facts.css_module_value_definition_names.clone(),
2890        css_module_value_import_edges: entry
2891            .facts
2892            .css_module_value_import_edges
2893            .iter()
2894            .map(|edge| omena_semantic::CssModulesValueImportEdgeFactV0 {
2895                remote_name: edge.remote_name.clone(),
2896                local_name: edge.local_name.clone(),
2897                import_source: edge.import_source.clone(),
2898            })
2899            .collect(),
2900        css_module_value_definition_edges: entry
2901            .facts
2902            .css_module_value_definition_edges
2903            .iter()
2904            .map(|edge| omena_semantic::CssModulesValueDefinitionEdgeFactV0 {
2905                definition_name: edge.definition_name.clone(),
2906                reference_names: edge.reference_names.clone(),
2907            })
2908            .collect(),
2909        css_module_composes_edges: entry
2910            .facts
2911            .css_module_composes_edges
2912            .iter()
2913            .map(|edge| omena_semantic::CssModulesComposesEdgeFactV0 {
2914                kind: edge.kind,
2915                owner_selector_names: edge.owner_selector_names.clone(),
2916                target_names: edge.target_names.clone(),
2917                import_source: edge.import_source.clone(),
2918            })
2919            .collect(),
2920        icss_export_names: entry.facts.icss_export_names.clone(),
2921        icss_import_edges: entry
2922            .facts
2923            .icss_import_edges
2924            .iter()
2925            .map(|edge| omena_semantic::CssModulesIcssImportEdgeFactV0 {
2926                local_name: edge.local_name.clone(),
2927                remote_name: edge.remote_name.clone(),
2928                import_source: edge.import_source.clone(),
2929            })
2930            .collect(),
2931        icss_export_edges: entry
2932            .facts
2933            .icss_export_edges
2934            .iter()
2935            .map(|edge| omena_semantic::CssModulesIcssExportEdgeFactV0 {
2936                export_name: edge.export_name.clone(),
2937                reference_names: edge.reference_names.clone(),
2938            })
2939            .collect(),
2940    }
2941}
2942
2943fn semantic_package_manifests_for_query(
2944    package_manifests: &[OmenaQueryStylePackageManifestV0],
2945) -> Vec<OmenaResolverStylePackageManifestV0> {
2946    package_manifests
2947        .iter()
2948        .map(|manifest| OmenaResolverStylePackageManifestV0 {
2949            package_json_path: manifest.package_json_path.clone(),
2950            package_json_source: manifest.package_json_source.clone(),
2951        })
2952        .collect()
2953}
2954
2955fn style_import_reachability_edges_for_query(
2956    style_fact_entries: &[OmenaQueryStyleFactEntry],
2957    package_manifests: &[OmenaQueryStylePackageManifestV0],
2958    resolution_inputs: &OmenaQueryStyleResolutionInputsV0,
2959) -> Vec<omena_semantic::StyleImportReachabilityEdgeFactV0> {
2960    let available_style_paths = style_fact_entries
2961        .iter()
2962        .map(|entry| entry.style_path.as_str())
2963        .collect::<BTreeSet<_>>();
2964    let mut edges = Vec::new();
2965    for entry in style_fact_entries {
2966        let targets = collect_style_module_dependency_sources_from_facts(&entry.facts)
2967            .into_iter()
2968            .filter_map(|source| {
2969                resolve_style_module_source_with_resolution_inputs(
2970                    entry.style_path.as_str(),
2971                    &source,
2972                    &available_style_paths,
2973                    package_manifests,
2974                    resolution_inputs,
2975                )
2976            })
2977            .collect::<BTreeSet<_>>();
2978        for target in targets {
2979            edges.push(omena_semantic::StyleImportReachabilityEdgeFactV0 {
2980                from_style_path: entry.style_path.clone(),
2981                target_style_path: target,
2982            });
2983        }
2984    }
2985    edges
2986}
2987
2988#[derive(Debug, Clone, PartialEq, Eq, PartialOrd, Ord)]
2989struct CssModulesComposesNode {
2990    style_path: String,
2991    selector_name: String,
2992}
2993
2994fn collect_css_modules_composes_adjacency(
2995    facts_by_path: &BTreeMap<&str, OmenaQueryOmenaParserStyleFactsV0>,
2996    available_style_paths: &BTreeSet<&str>,
2997    package_manifests: &[OmenaQueryStylePackageManifestV0],
2998) -> BTreeMap<CssModulesComposesNode, BTreeSet<CssModulesComposesNode>> {
2999    collect_css_modules_composes_adjacency_with_path_mappings(
3000        facts_by_path,
3001        available_style_paths,
3002        package_manifests,
3003        &[],
3004        &[],
3005        &[],
3006    )
3007}
3008
3009fn collect_css_modules_composes_adjacency_with_path_mappings(
3010    facts_by_path: &BTreeMap<&str, OmenaQueryOmenaParserStyleFactsV0>,
3011    available_style_paths: &BTreeSet<&str>,
3012    package_manifests: &[OmenaQueryStylePackageManifestV0],
3013    bundler_path_mappings: &[OmenaResolverBundlerPathAliasMappingV0],
3014    tsconfig_path_mappings: &[OmenaResolverTsconfigPathMappingV0],
3015    disk_style_path_identities: &[OmenaResolverStyleModuleDiskCandidateIdentityV0],
3016) -> BTreeMap<CssModulesComposesNode, BTreeSet<CssModulesComposesNode>> {
3017    let mut graph = BTreeMap::new();
3018    for (style_path, facts) in facts_by_path {
3019        let class_names = facts
3020            .class_selector_names
3021            .iter()
3022            .map(String::as_str)
3023            .collect::<BTreeSet<_>>();
3024        for edge in &facts.css_module_composes_edges {
3025            if edge.kind == "global" {
3026                continue;
3027            }
3028            let target_style_path = if edge.kind == "external" {
3029                edge.import_source.as_deref().and_then(|source| {
3030                    resolve_style_module_source_with_path_mappings(
3031                        style_path,
3032                        source,
3033                        available_style_paths,
3034                        package_manifests,
3035                        bundler_path_mappings,
3036                        tsconfig_path_mappings,
3037                        disk_style_path_identities,
3038                    )
3039                })
3040            } else {
3041                Some((*style_path).to_string())
3042            };
3043            let Some(target_style_path) = target_style_path else {
3044                continue;
3045            };
3046            let target_class_names = if target_style_path == *style_path {
3047                class_names.clone()
3048            } else {
3049                facts_by_path
3050                    .get(target_style_path.as_str())
3051                    .map(|facts| {
3052                        facts
3053                            .class_selector_names
3054                            .iter()
3055                            .map(String::as_str)
3056                            .collect::<BTreeSet<_>>()
3057                    })
3058                    .unwrap_or_default()
3059            };
3060            for owner_selector_name in &edge.owner_selector_names {
3061                if !class_names.contains(owner_selector_name.as_str()) {
3062                    continue;
3063                }
3064                let owner = CssModulesComposesNode {
3065                    style_path: (*style_path).to_string(),
3066                    selector_name: owner_selector_name.clone(),
3067                };
3068                for target_selector_name in &edge.target_names {
3069                    if !target_class_names.contains(target_selector_name.as_str()) {
3070                        continue;
3071                    }
3072                    graph
3073                        .entry(owner.clone())
3074                        .or_insert_with(BTreeSet::new)
3075                        .insert(CssModulesComposesNode {
3076                            style_path: target_style_path.clone(),
3077                            selector_name: target_selector_name.clone(),
3078                        });
3079                }
3080            }
3081        }
3082    }
3083    graph
3084}
3085
3086fn filter_import_reachable_design_token_workspace_declarations(
3087    target_style_path: &str,
3088    style_fact_entries: &[OmenaQueryStyleFactEntry],
3089    workspace_declarations: &[DesignTokenWorkspaceDeclarationFactV0],
3090    package_manifests: &[OmenaQueryStylePackageManifestV0],
3091    bundler_path_mappings: &[OmenaResolverBundlerPathAliasMappingV0],
3092    tsconfig_path_mappings: &[OmenaResolverTsconfigPathMappingV0],
3093    disk_style_path_identities: &[OmenaResolverStyleModuleDiskCandidateIdentityV0],
3094) -> Vec<DesignTokenWorkspaceDeclarationFactV0> {
3095    let reachable_style_paths = collect_import_reachable_style_path_metadata(
3096        target_style_path,
3097        style_fact_entries,
3098        package_manifests,
3099        bundler_path_mappings,
3100        tsconfig_path_mappings,
3101        disk_style_path_identities,
3102    );
3103    workspace_declarations
3104        .iter()
3105        .filter_map(|declaration| {
3106            if declaration.file_path == target_style_path {
3107                return Some(declaration.clone());
3108            }
3109            let reachability = reachable_style_paths.get(declaration.file_path.as_str())?;
3110            let mut declaration = declaration.clone();
3111            declaration.import_graph_distance = Some(reachability.distance);
3112            declaration.import_graph_order = Some(reachability.order);
3113            Some(declaration)
3114        })
3115        .collect()
3116}
3117
3118#[derive(Debug, Clone, Copy, PartialEq, Eq)]
3119struct ImportReachability {
3120    distance: usize,
3121    order: usize,
3122}
3123
3124fn collect_import_reachable_style_path_metadata(
3125    target_style_path: &str,
3126    style_fact_entries: &[OmenaQueryStyleFactEntry],
3127    package_manifests: &[OmenaQueryStylePackageManifestV0],
3128    bundler_path_mappings: &[OmenaResolverBundlerPathAliasMappingV0],
3129    tsconfig_path_mappings: &[OmenaResolverTsconfigPathMappingV0],
3130    disk_style_path_identities: &[OmenaResolverStyleModuleDiskCandidateIdentityV0],
3131) -> BTreeMap<String, ImportReachability> {
3132    let available_style_paths = style_fact_entries
3133        .iter()
3134        .map(|entry| entry.style_path.as_str())
3135        .collect::<BTreeSet<_>>();
3136    let mut edges = Vec::new();
3137    for entry in style_fact_entries {
3138        let targets = collect_style_module_dependency_sources_from_facts(&entry.facts)
3139            .into_iter()
3140            .filter_map(|source| {
3141                resolve_style_module_source_with_path_mappings(
3142                    entry.style_path.as_str(),
3143                    &source,
3144                    &available_style_paths,
3145                    package_manifests,
3146                    bundler_path_mappings,
3147                    tsconfig_path_mappings,
3148                    disk_style_path_identities,
3149                )
3150            })
3151            .collect::<BTreeSet<_>>();
3152        for target in targets {
3153            edges.push(omena_semantic::StyleImportReachabilityEdgeFactV0 {
3154                from_style_path: entry.style_path.clone(),
3155                target_style_path: target,
3156            });
3157        }
3158    }
3159
3160    omena_semantic::summarize_style_import_reachability(target_style_path, edges.as_slice())
3161        .reachable_style_paths
3162        .into_iter()
3163        .map(|fact| {
3164            (
3165                fact.style_path,
3166                ImportReachability {
3167                    distance: fact.distance,
3168                    order: fact.order,
3169                },
3170            )
3171        })
3172        .collect()
3173}
3174
3175fn collect_style_module_dependency_sources_from_facts(
3176    facts: &OmenaQueryOmenaParserStyleFactsV0,
3177) -> Vec<String> {
3178    let mut sources = facts
3179        .sass_module_edges
3180        .iter()
3181        .map(|edge| edge.source.clone())
3182        .collect::<Vec<_>>();
3183    sources.extend(
3184        facts
3185            .css_module_value_import_edges
3186            .iter()
3187            .map(|edge| edge.import_source.clone()),
3188    );
3189    sources.extend(
3190        facts
3191            .css_module_composes_edges
3192            .iter()
3193            .filter_map(|edge| edge.import_source.clone()),
3194    );
3195    sources.extend(
3196        facts
3197            .icss_import_edges
3198            .iter()
3199            .map(|edge| edge.import_source.clone()),
3200    );
3201    sources.sort();
3202    sources.dedup();
3203    sources
3204}
3205
3206fn resolve_style_module_source_with_resolution_inputs(
3207    from_style_path: &str,
3208    source: &str,
3209    available_style_paths: &BTreeSet<&str>,
3210    package_manifests: &[OmenaQueryStylePackageManifestV0],
3211    resolution_inputs: &OmenaQueryStyleResolutionInputsV0,
3212) -> Option<String> {
3213    resolve_style_module_source_with_path_mappings(
3214        from_style_path,
3215        source,
3216        available_style_paths,
3217        package_manifests,
3218        resolution_inputs.bundler_path_mappings.as_slice(),
3219        resolution_inputs.tsconfig_path_mappings.as_slice(),
3220        resolution_inputs.disk_style_path_identities.as_slice(),
3221    )
3222}
3223
3224/// Resolves style-module specifiers through the shared package, alias, load-path, and disk-identity
3225/// authority so every query surface observes the same workspace routing inputs.
3226fn resolve_style_module_source_with_path_mappings(
3227    from_style_path: &str,
3228    source: &str,
3229    available_style_paths: &BTreeSet<&str>,
3230    package_manifests: &[OmenaQueryStylePackageManifestV0],
3231    bundler_path_mappings: &[OmenaResolverBundlerPathAliasMappingV0],
3232    tsconfig_path_mappings: &[OmenaResolverTsconfigPathMappingV0],
3233    disk_style_path_identities: &[OmenaResolverStyleModuleDiskCandidateIdentityV0],
3234) -> Option<String> {
3235    resolve_style_module_source_with_path_mappings_and_identity_index(
3236        from_style_path,
3237        source,
3238        available_style_paths,
3239        package_manifests,
3240        bundler_path_mappings,
3241        tsconfig_path_mappings,
3242        disk_style_path_identities,
3243        None,
3244    )
3245}
3246
3247#[allow(clippy::too_many_arguments)]
3248fn resolve_style_module_source_with_path_mappings_and_identity_index(
3249    from_style_path: &str,
3250    source: &str,
3251    available_style_paths: &BTreeSet<&str>,
3252    package_manifests: &[OmenaQueryStylePackageManifestV0],
3253    bundler_path_mappings: &[OmenaResolverBundlerPathAliasMappingV0],
3254    tsconfig_path_mappings: &[OmenaResolverTsconfigPathMappingV0],
3255    disk_style_path_identities: &[OmenaResolverStyleModuleDiskCandidateIdentityV0],
3256    identity_index: Option<&OmenaResolverStyleModuleConfirmationIdentityIndexV0>,
3257) -> Option<String> {
3258    let load_path_roots = collect_load_path_roots(available_style_paths);
3259    let load_path_root_refs = load_path_roots
3260        .iter()
3261        .map(String::as_str)
3262        .collect::<Vec<_>>();
3263    let resolver_package_manifests = package_manifests
3264        .iter()
3265        .map(|manifest| OmenaResolverStylePackageManifestV0 {
3266            package_json_path: manifest.package_json_path.clone(),
3267            package_json_source: manifest.package_json_source.clone(),
3268        })
3269        .collect::<Vec<_>>();
3270    summarize_omena_resolver_style_module_resolution_with_confirmation_inputs(
3271        from_style_path,
3272        source,
3273        available_style_paths,
3274        disk_style_path_identities,
3275        &resolver_package_manifests,
3276        bundler_path_mappings,
3277        tsconfig_path_mappings,
3278        load_path_root_refs.as_slice(),
3279        OmenaResolverStyleModuleConfirmationOptionsV0 {
3280            allow_disk_confirmation: true,
3281            identity_index,
3282            ..OmenaResolverStyleModuleConfirmationOptionsV0::default()
3283        },
3284    )
3285    .resolved_style_path
3286}
3287
3288fn collect_style_selector_hover_candidates_from_omena_parser_facts(
3289    source: &str,
3290    definition_facts: &[ParsedSelectorFact],
3291    seen: &mut BTreeSet<(usize, usize, String)>,
3292    candidates: &mut Vec<OmenaQueryStyleHoverCandidateV0>,
3293) {
3294    for fact in definition_facts {
3295        if fact.kind != ParsedSelectorFactKind::Class {
3296            continue;
3297        }
3298        let start: u32 = fact.range.start().into();
3299        let end: u32 = fact.range.end().into();
3300        let byte_span = ParserByteSpanV0 {
3301            start: start as usize,
3302            end: end as usize,
3303        };
3304        if seen.insert((byte_span.start, byte_span.end, fact.name.clone())) {
3305            candidates.push(OmenaQueryStyleHoverCandidateV0 {
3306                kind: "selector",
3307                name: fact.name.clone(),
3308                range: parser_range_for_byte_span(source, byte_span),
3309                source: "omenaParserSelectorFacts",
3310                namespace: None,
3311            });
3312        }
3313    }
3314}
3315
3316fn collect_custom_property_hover_candidates_from_omena_parser_facts(
3317    source: &str,
3318    variable_facts: &[ParsedVariableFact],
3319    seen: &mut BTreeSet<(usize, usize, String)>,
3320    candidates: &mut Vec<OmenaQueryStyleHoverCandidateV0>,
3321) {
3322    for fact in variable_facts {
3323        let kind = match fact.kind {
3324            ParsedVariableFactKind::CustomPropertyDeclaration => "customPropertyDeclaration",
3325            ParsedVariableFactKind::CustomPropertyReference => "customPropertyReference",
3326            _ => continue,
3327        };
3328        let start: u32 = fact.range.start().into();
3329        let end: u32 = fact.range.end().into();
3330        let byte_span = ParserByteSpanV0 {
3331            start: start as usize,
3332            end: end as usize,
3333        };
3334        if seen.insert((byte_span.start, byte_span.end, fact.name.clone())) {
3335            candidates.push(OmenaQueryStyleHoverCandidateV0 {
3336                kind,
3337                name: fact.name.clone(),
3338                range: parser_range_for_byte_span(source, byte_span),
3339                source: "omenaParserVariableFacts",
3340                namespace: None,
3341            });
3342        }
3343    }
3344}
3345
3346fn collect_sass_symbol_hover_candidates_from_omena_parser_facts(
3347    source: &str,
3348    symbol_facts: &[omena_parser::ParsedSassSymbolFact],
3349    seen: &mut BTreeSet<(usize, usize, String)>,
3350    candidates: &mut Vec<OmenaQueryStyleHoverCandidateV0>,
3351) {
3352    for fact in symbol_facts {
3353        let kind = match fact.kind {
3354            ParsedSassSymbolFactKind::VariableDeclaration
3355            | ParsedSassSymbolFactKind::MixinDeclaration
3356            | ParsedSassSymbolFactKind::FunctionDeclaration => {
3357                sass_symbol_declaration_candidate_kind(fact.symbol_kind)
3358            }
3359            ParsedSassSymbolFactKind::VariableReference
3360            | ParsedSassSymbolFactKind::MixinInclude
3361            | ParsedSassSymbolFactKind::FunctionCall => {
3362                sass_symbol_reference_candidate_kind(fact.symbol_kind, fact.role)
3363            }
3364        };
3365        let start: u32 = fact.range.start().into();
3366        let end: u32 = fact.range.end().into();
3367        let byte_span = ParserByteSpanV0 {
3368            start: start as usize,
3369            end: end as usize,
3370        };
3371        if seen.insert((
3372            byte_span.start,
3373            byte_span.end,
3374            format!(
3375                "{}:{}:{}",
3376                fact.symbol_kind,
3377                fact.namespace.as_deref().unwrap_or_default(),
3378                fact.name
3379            ),
3380        )) {
3381            candidates.push(OmenaQueryStyleHoverCandidateV0 {
3382                kind,
3383                name: fact.name.clone(),
3384                range: parser_range_for_byte_span(source, byte_span),
3385                source: "omenaParserSassSymbolFacts",
3386                namespace: fact.namespace.clone(),
3387            });
3388        }
3389    }
3390}
3391
3392fn collect_sass_partial_evaluator_selector_candidates_from_omena_parser_facts(
3393    source: &str,
3394    includes: &[ParsedSassIncludeFact],
3395    seen: &mut BTreeSet<(usize, usize, String)>,
3396    candidates: &mut Vec<OmenaQueryStyleHoverCandidateV0>,
3397) {
3398    for include in includes {
3399        let start: u32 = include.range.start().into();
3400        let end: u32 = include.range.end().into();
3401        let range_span = ParserByteSpanV0 {
3402            start: start as usize,
3403            end: end as usize,
3404        };
3405        for selector_name in infer_sass_include_generated_selector_names(&include.params) {
3406            if seen.insert((range_span.start, range_span.end, selector_name.clone())) {
3407                candidates.push(OmenaQueryStyleHoverCandidateV0 {
3408                    kind: "selector",
3409                    name: selector_name,
3410                    range: parser_range_for_byte_span(source, range_span),
3411                    source: "sassPartialEvaluatorGeneratedSelectors",
3412                    namespace: None,
3413                });
3414            }
3415        }
3416    }
3417}
3418
3419fn infer_sass_include_generated_selector_names(params: &str) -> Vec<String> {
3420    let Some(prefix) = sass_named_argument_string_value(params, "prefix") else {
3421        return Vec::new();
3422    };
3423    if prefix.is_empty() || !prefix.chars().all(is_css_identifier_continue) {
3424        return Vec::new();
3425    }
3426    let mut selectors = sass_first_map_string_keys(params)
3427        .into_iter()
3428        .filter(|key| !key.is_empty() && key.chars().all(is_css_identifier_continue))
3429        .map(|key| format!("{prefix}-{key}"))
3430        .collect::<Vec<_>>();
3431    selectors.sort();
3432    selectors.dedup();
3433    selectors
3434}
3435
3436fn sass_named_argument_string_value(params: &str, name: &str) -> Option<String> {
3437    let needle = format!("${name}");
3438    let mut cursor = 0usize;
3439    while let Some(relative_match) = params[cursor..].find(needle.as_str()) {
3440        let name_start = cursor + relative_match;
3441        let name_end = name_start + needle.len();
3442        if !sass_identifier_boundary(params, name_start, name_end) {
3443            cursor = name_end;
3444            continue;
3445        }
3446        let colon_offset = skip_ascii_whitespace(params, name_end);
3447        if params.as_bytes().get(colon_offset) != Some(&b':') {
3448            cursor = name_end;
3449            continue;
3450        }
3451        let value_start = skip_ascii_whitespace(params, colon_offset + 1);
3452        return sass_string_literal_value(params, value_start).map(|(value, _)| value);
3453    }
3454    None
3455}
3456
3457fn sass_first_map_string_keys(params: &str) -> Vec<String> {
3458    let mut cursor = 0usize;
3459    while cursor < params.len() {
3460        let Some(open_relative) = params[cursor..].find('(') else {
3461            break;
3462        };
3463        let open = cursor + open_relative;
3464        let Some(close) = matching_style_block_end(params, open, b'(', b')') else {
3465            break;
3466        };
3467        let keys = sass_map_string_keys(params, open + 1, close);
3468        if !keys.is_empty() {
3469            return keys;
3470        }
3471        cursor = open + 1;
3472    }
3473    Vec::new()
3474}
3475
3476fn sass_map_string_keys(params: &str, start: usize, end: usize) -> Vec<String> {
3477    split_top_level_style_segments(params, start, end, b',')
3478        .into_iter()
3479        .filter_map(|(entry_start, entry_end)| {
3480            let key_start = skip_ascii_whitespace(params, entry_start);
3481            let (key, key_end) = sass_string_literal_value(params, key_start)?;
3482            let colon_offset = skip_ascii_whitespace(params, key_end);
3483            (colon_offset < entry_end && params.as_bytes().get(colon_offset) == Some(&b':'))
3484                .then_some(key)
3485        })
3486        .collect()
3487}
3488
3489fn sass_string_literal_value(source: &str, quote_offset: usize) -> Option<(String, usize)> {
3490    let quote = source.as_bytes().get(quote_offset).copied()?;
3491    if !matches!(quote, b'\'' | b'"') {
3492        return None;
3493    }
3494    let literal_end = skip_style_string_literal(source, quote_offset, source.len())?;
3495    let value_end = literal_end.saturating_sub(1);
3496    source
3497        .get(quote_offset + 1..value_end)
3498        .map(|value| (value.to_string(), literal_end))
3499}
3500
3501fn sass_identifier_boundary(source: &str, start: usize, end: usize) -> bool {
3502    let before = source
3503        .get(..start)
3504        .and_then(|prefix| prefix.chars().next_back())
3505        .is_none_or(|ch| !is_css_identifier_continue(ch) && ch != '$');
3506    let after = source
3507        .get(end..)
3508        .and_then(|suffix| suffix.chars().next())
3509        .is_none_or(|ch| !is_css_identifier_continue(ch));
3510    before && after
3511}
3512
3513fn sass_symbol_declaration_candidate_kind(symbol_kind: &str) -> &'static str {
3514    match symbol_kind {
3515        "variable" => "sassVariableDeclaration",
3516        "mixin" => "sassMixinDeclaration",
3517        "function" => "sassFunctionDeclaration",
3518        _ => "sassSymbolDeclaration",
3519    }
3520}
3521
3522fn is_sass_symbol_candidate_kind(kind: &str) -> bool {
3523    sass_symbol_kind_from_candidate_kind(kind).is_some()
3524}
3525
3526fn is_sass_symbol_declaration_kind(kind: &str) -> bool {
3527    matches!(
3528        kind,
3529        "sassVariableDeclaration"
3530            | "sassMixinDeclaration"
3531            | "sassFunctionDeclaration"
3532            | "sassSymbolDeclaration"
3533    )
3534}
3535
3536fn sass_symbol_kind_from_candidate_kind(kind: &str) -> Option<&'static str> {
3537    match kind {
3538        "sassVariableDeclaration" | "sassVariableReference" => Some("variable"),
3539        "sassMixinDeclaration" | "sassMixinInclude" | "sassMixinReference" => Some("mixin"),
3540        "sassFunctionDeclaration" | "sassFunctionCall" | "sassFunctionReference" => {
3541            Some("function")
3542        }
3543        "sassSymbolDeclaration" | "sassSymbolReference" => Some("symbol"),
3544        _ => None,
3545    }
3546}
3547
3548fn sass_symbol_reference_candidate_kind(symbol_kind: &str, role: &str) -> &'static str {
3549    match (symbol_kind, role) {
3550        ("variable", _) => "sassVariableReference",
3551        ("mixin", "include") => "sassMixinInclude",
3552        ("function", "call") => "sassFunctionCall",
3553        ("mixin", _) => "sassMixinReference",
3554        ("function", _) => "sassFunctionReference",
3555        _ => "sassSymbolReference",
3556    }
3557}
3558
3559fn sass_variable_value_from_declaration_line(line: &str) -> Option<String> {
3560    let (_, value) = line.split_once(':')?;
3561    let value = value
3562        .trim()
3563        .trim_end_matches(';')
3564        .trim()
3565        .trim_end_matches("!default")
3566        .trim();
3567    (!value.is_empty()).then(|| value.to_string())
3568}
3569
3570fn sass_callable_definition_render_parts(
3571    source: &str,
3572    position: ParserPositionV0,
3573) -> Option<(String, String)> {
3574    let line_start = byte_offset_for_parser_position(
3575        source,
3576        ParserPositionV0 {
3577            line: position.line,
3578            character: 0,
3579        },
3580    )?;
3581    let open_brace = source[line_start..].find('{')? + line_start;
3582    let close_brace = matching_style_block_end(source, open_brace, b'{', b'}')?;
3583    let signature = source[line_start..open_brace].trim().to_string();
3584    let body = source[open_brace + 1..close_brace].trim();
3585    if signature.is_empty() || body.is_empty() {
3586        return None;
3587    }
3588    Some((
3589        signature,
3590        trim_hover_snippet(dedent_hover_body(body).as_str()),
3591    ))
3592}
3593
3594/// A block body is extracted mid-source, so `trim` strips the FIRST line's
3595/// indentation while continuation lines keep the source's: the hover then
3596/// renders line one flush left and everything after it indented. Re-align
3597/// by removing the continuation lines' common leading whitespace; relative
3598/// indentation (nested rules) survives.
3599fn dedent_hover_body(body: &str) -> String {
3600    fn leading_whitespace_bytes(line: &str) -> usize {
3601        line.len() - line.trim_start().len()
3602    }
3603    let common = body
3604        .lines()
3605        .skip(1)
3606        .filter(|line| !line.trim().is_empty())
3607        .map(leading_whitespace_bytes)
3608        .min()
3609        .unwrap_or(0);
3610    if common == 0 {
3611        return body.to_string();
3612    }
3613    let mut lines = body.lines();
3614    let mut dedented = lines.next().unwrap_or_default().to_string();
3615    for line in lines {
3616        dedented.push('\n');
3617        let mut stripped = 0usize;
3618        for (offset, character) in line.char_indices() {
3619            if stripped >= common || !character.is_whitespace() {
3620                dedented.push_str(&line[offset..]);
3621                break;
3622            }
3623            stripped += character.len_utf8();
3624        }
3625    }
3626    dedented
3627}
3628
3629fn rule_snippet_around_position(source: &str, position: ParserPositionV0) -> Option<String> {
3630    let line_start = byte_offset_for_parser_position(
3631        source,
3632        ParserPositionV0 {
3633            line: position.line,
3634            character: 0,
3635        },
3636    )?;
3637    let open_brace = source[line_start..].find('{')? + line_start;
3638    let mut depth = 0usize;
3639    let mut cursor = open_brace;
3640    while cursor < source.len() {
3641        match source.as_bytes().get(cursor).copied()? {
3642            b'{' => depth += 1,
3643            b'}' => {
3644                depth = depth.saturating_sub(1);
3645                if depth == 0 {
3646                    let snippet = source[line_start..=cursor].trim();
3647                    return Some(trim_hover_snippet(snippet));
3648                }
3649            }
3650            _ => {}
3651        }
3652        cursor = advance_style_scan_cursor(source, cursor, source.len());
3653    }
3654    None
3655}
3656
3657fn line_snippet_at_position(source: &str, position: ParserPositionV0) -> Option<String> {
3658    let line_start = byte_offset_for_parser_position(
3659        source,
3660        ParserPositionV0 {
3661            line: position.line,
3662            character: 0,
3663        },
3664    )?;
3665    let line_end = source[line_start..]
3666        .find('\n')
3667        .map(|offset| line_start + offset)
3668        .unwrap_or(source.len());
3669    Some(source[line_start..line_end].trim().to_string())
3670}
3671
3672fn style_completion_context_at_position(
3673    source: &str,
3674    position: ParserPositionV0,
3675) -> Option<(&'static str, Option<String>)> {
3676    let cursor = byte_offset_for_parser_position(source, position)?;
3677    let line_start = byte_offset_for_parser_position(
3678        source,
3679        ParserPositionV0 {
3680            line: position.line,
3681            character: 0,
3682        },
3683    )?;
3684    let line_prefix = source.get(line_start..cursor)?;
3685    if let Some(var_start) = line_prefix.rfind("var(") {
3686        let var_prefix = &line_prefix[var_start + "var(".len()..];
3687        if !var_prefix.contains(')') {
3688            let prefix = var_prefix
3689                .rsplit(|ch: char| ch == ',' || ch.is_ascii_whitespace())
3690                .next()
3691                .unwrap_or_default();
3692            let prefix = (!prefix.is_empty()).then(|| prefix.to_string());
3693            return Some(("styleCustomPropertyReference", prefix));
3694        }
3695    }
3696    if let Some(prefix) = sass_variable_completion_prefix(line_prefix) {
3697        return Some(("sassVariableReference", Some(prefix)));
3698    }
3699    if let Some(prefix) = sass_mixin_completion_prefix(line_prefix) {
3700        return Some(("sassMixinReference", prefix));
3701    }
3702    if let Some(prefix) = sass_member_completion_prefix(line_prefix) {
3703        return Some(("sassMemberReference", Some(prefix)));
3704    }
3705
3706    Some(("styleDocument", None))
3707}
3708
3709fn sass_variable_completion_prefix(line_prefix: &str) -> Option<String> {
3710    let token = sass_completion_trailing_token(line_prefix)?;
3711    let dollar_offset = token.rfind('$')?;
3712    let suffix = token.get(dollar_offset + 1..)?;
3713    if !suffix.chars().all(is_sass_completion_identifier_continue) {
3714        return None;
3715    }
3716    let prefix = token.get(..)?;
3717    (!prefix.is_empty()).then(|| prefix.to_string())
3718}
3719
3720fn sass_mixin_completion_prefix(line_prefix: &str) -> Option<Option<String>> {
3721    let include_offset = line_prefix.rfind("@include")?;
3722    let after_include = line_prefix.get(include_offset + "@include".len()..)?;
3723    if after_include.contains(';') || after_include.contains('{') || after_include.contains('}') {
3724        return None;
3725    }
3726    let token = sass_completion_trailing_token(after_include.trim_start())?;
3727    if token.contains('$') || !token.chars().all(is_sass_completion_member_continue) {
3728        return None;
3729    }
3730    Some((!token.is_empty()).then(|| token.to_string()))
3731}
3732
3733fn sass_member_completion_prefix(line_prefix: &str) -> Option<String> {
3734    let token = sass_completion_trailing_token(line_prefix)?;
3735    if token.starts_with('.') || token.contains('$') || !token.contains('.') {
3736        return None;
3737    }
3738    if !token.chars().all(is_sass_completion_member_continue) {
3739        return None;
3740    }
3741    let (namespace, _) = token.split_once('.')?;
3742    (!namespace.is_empty()).then(|| token.to_string())
3743}
3744
3745fn sass_completion_trailing_token(text: &str) -> Option<&str> {
3746    text.rsplit(|ch: char| {
3747        ch.is_ascii_whitespace()
3748            || matches!(ch, ':' | ';' | '{' | '}' | '(' | ')' | ',' | '[' | ']')
3749    })
3750    .next()
3751    .filter(|token| !token.is_empty())
3752}
3753
3754fn is_sass_completion_identifier_continue(ch: char) -> bool {
3755    is_css_identifier_continue(ch)
3756}
3757
3758fn is_sass_completion_member_continue(ch: char) -> bool {
3759    is_css_identifier_continue(ch) || ch == '.' || ch == '$'
3760}
3761
3762fn trim_hover_snippet(snippet: &str) -> String {
3763    const MAX_SNIPPET_LEN: usize = 1200;
3764    if snippet.len() <= MAX_SNIPPET_LEN {
3765        return snippet.to_string();
3766    }
3767    let end = char_boundary_floor(snippet, MAX_SNIPPET_LEN);
3768    format!("{}...", snippet[..end].trim_end())
3769}
3770
3771fn is_css_identifier_continue(ch: char) -> bool {
3772    ch.is_ascii_alphanumeric() || matches!(ch, '-' | '_')
3773}
3774
3775fn parser_range_for_byte_span(source: &str, span: ParserByteSpanV0) -> ParserRangeV0 {
3776    ParserRangeV0 {
3777        start: parser_position_for_byte_offset(source, span.start),
3778        end: parser_position_for_byte_offset(source, span.end),
3779    }
3780}
3781
3782fn push_omena_query_ready_surface(ready_surfaces: &mut Vec<&'static str>, surface: &'static str) {
3783    if !ready_surfaces.contains(&surface) {
3784        ready_surfaces.push(surface);
3785    }
3786}
3787
3788fn end_of_source_range(source: &str) -> ParserRangeV0 {
3789    let position = parser_position_for_byte_offset(source, source.len());
3790    ParserRangeV0 {
3791        start: position,
3792        end: position,
3793    }
3794}
3795
3796fn parser_position_for_byte_offset(source: &str, offset: usize) -> ParserPositionV0 {
3797    let clamped_offset = offset.min(source.len());
3798    let mut line = 0usize;
3799    let mut character = 0usize;
3800
3801    for (byte_index, ch) in source.char_indices() {
3802        if byte_index >= clamped_offset {
3803            break;
3804        }
3805        if ch == '\n' {
3806            line += 1;
3807            character = 0;
3808        } else {
3809            character += ch.len_utf16();
3810        }
3811    }
3812
3813    ParserPositionV0 { line, character }
3814}
3815
3816fn byte_offset_for_parser_position(source: &str, position: ParserPositionV0) -> Option<usize> {
3817    let mut current_line = 0usize;
3818    let mut current_character = 0usize;
3819
3820    if position.line == 0 && position.character == 0 {
3821        return Some(0);
3822    }
3823
3824    for (byte_index, ch) in source.char_indices() {
3825        if current_line == position.line && current_character == position.character {
3826            return Some(byte_index);
3827        }
3828        if ch == '\n' {
3829            current_line += 1;
3830            current_character = 0;
3831            if current_line == position.line && position.character == 0 {
3832                return Some(byte_index + ch.len_utf8());
3833            }
3834        } else if current_line == position.line {
3835            current_character += ch.len_utf16();
3836        }
3837    }
3838
3839    (current_line == position.line && current_character == position.character)
3840        .then_some(source.len())
3841}
3842
3843fn skip_ascii_whitespace(source: &str, mut offset: usize) -> usize {
3844    while source
3845        .as_bytes()
3846        .get(offset)
3847        .is_some_and(u8::is_ascii_whitespace)
3848    {
3849        offset += 1;
3850    }
3851    offset
3852}
3853
3854fn matching_style_block_end(
3855    source: &str,
3856    open_offset: usize,
3857    open: u8,
3858    close: u8,
3859) -> Option<usize> {
3860    if source.as_bytes().get(open_offset) != Some(&open) {
3861        return None;
3862    }
3863    let mut cursor = advance_style_scan_cursor(source, open_offset, source.len());
3864    let mut depth = 1usize;
3865    while cursor < source.len() {
3866        match source.as_bytes().get(cursor).copied()? {
3867            b'\'' | b'"' | b'`' => {
3868                cursor = skip_style_string_literal(source, cursor, source.len())?;
3869            }
3870            byte if byte == open => {
3871                depth += 1;
3872                cursor = advance_style_scan_cursor(source, cursor, source.len());
3873            }
3874            byte if byte == close => {
3875                depth -= 1;
3876                if depth == 0 {
3877                    return Some(cursor);
3878                }
3879                cursor = advance_style_scan_cursor(source, cursor, source.len());
3880            }
3881            _ => cursor = advance_style_scan_cursor(source, cursor, source.len()),
3882        }
3883    }
3884    None
3885}
3886
3887fn split_top_level_style_segments(
3888    source: &str,
3889    start: usize,
3890    end: usize,
3891    delimiter: u8,
3892) -> Vec<(usize, usize)> {
3893    let mut segments = Vec::new();
3894    let end = char_boundary_floor(source, end);
3895    let mut segment_start = char_boundary_ceil(source, start).min(end);
3896    let mut cursor = segment_start;
3897    let mut depth = 0usize;
3898    while cursor < end {
3899        match source.as_bytes().get(cursor).copied() {
3900            Some(b'\'' | b'"' | b'`') => {
3901                cursor = skip_style_string_literal(source, cursor, end).unwrap_or(end);
3902            }
3903            Some(b'(' | b'[' | b'{') => {
3904                depth += 1;
3905                cursor = advance_style_scan_cursor(source, cursor, end);
3906            }
3907            Some(b')' | b']' | b'}') => {
3908                depth = depth.saturating_sub(1);
3909                cursor = advance_style_scan_cursor(source, cursor, end);
3910            }
3911            Some(byte) if byte == delimiter && depth == 0 => {
3912                segments.push((segment_start, cursor));
3913                cursor = advance_style_scan_cursor(source, cursor, end);
3914                segment_start = cursor;
3915            }
3916            Some(_) => cursor = advance_style_scan_cursor(source, cursor, end),
3917            None => break,
3918        }
3919    }
3920    if segment_start <= end {
3921        segments.push((segment_start, end));
3922    }
3923    segments
3924}
3925
3926fn skip_style_string_literal(source: &str, quote_offset: usize, limit: usize) -> Option<usize> {
3927    let quote = source.as_bytes().get(quote_offset).copied()?;
3928    let limit = char_boundary_floor(source, limit);
3929    let mut cursor = quote_offset + 1;
3930    while cursor < limit {
3931        let byte = source.as_bytes().get(cursor).copied()?;
3932        if byte == b'\\' {
3933            cursor = advance_style_escaped_char(source, cursor, limit);
3934            continue;
3935        }
3936        if byte == quote {
3937            return Some(cursor + 1);
3938        }
3939        cursor = advance_style_scan_cursor(source, cursor, limit);
3940    }
3941    None
3942}
3943
3944fn advance_style_escaped_char(source: &str, slash_offset: usize, limit: usize) -> usize {
3945    let after_slash = advance_style_scan_cursor(source, slash_offset, limit);
3946    advance_style_scan_cursor(source, after_slash, limit)
3947}
3948
3949fn advance_style_scan_cursor(source: &str, cursor: usize, limit: usize) -> usize {
3950    let cursor = char_boundary_ceil(source, cursor);
3951    let limit = char_boundary_floor(source, limit);
3952    if cursor >= limit {
3953        return limit;
3954    }
3955    char_boundary_ceil(source, cursor + 1).min(limit)
3956}
3957
3958fn char_boundary_floor(source: &str, index: usize) -> usize {
3959    let mut index = index.min(source.len());
3960    while index > 0 && !source.is_char_boundary(index) {
3961        index -= 1;
3962    }
3963    index
3964}
3965
3966fn char_boundary_ceil(source: &str, index: usize) -> usize {
3967    let mut index = index.min(source.len());
3968    while index < source.len() && !source.is_char_boundary(index) {
3969        index += 1;
3970    }
3971    index
3972}
3973
3974fn is_sass_builtin_module_source(source: &str) -> bool {
3975    source.starts_with("sass:")
3976}
3977
3978fn format_query_sass_symbol_label(symbol_kind: &str, name: &str) -> String {
3979    match symbol_kind {
3980        "variable" => format!("Sass variable '${name}'"),
3981        "mixin" => format!("Sass mixin '@mixin {name}'"),
3982        "function" => format!("Sass function '{name}()'"),
3983        _ => format!("Sass symbol '{name}'"),
3984    }
3985}
3986
3987#[cfg(test)]
3988mod runtime_index_tests {
3989    use super::*;
3990
3991    #[test]
3992    fn semantic_runtime_index_from_query_facts_matches_source_parser() {
3993        let style_path = "/workspace/src/App.module.scss";
3994        let style_source = r#"
3995@keyframes fade { to { opacity: 1; } }
3996.card {
3997  --brand: red;
3998  color: var(--brand);
3999  animation: fade 1s;
4000}
4001"#;
4002        let facts = summarize_omena_query_omena_parser_style_facts(
4003            style_source,
4004            omena_parser_dialect_for_style_path(style_path),
4005        );
4006
4007        assert_eq!(
4008            semantic_runtime_index_from_query_style_facts(style_path, &facts),
4009            omena_semantic::summarize_style_runtime_index_facts_from_source(
4010                style_path,
4011                style_source,
4012            ),
4013        );
4014    }
4015}