Skip to main content

omena_query/
style.rs

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