Skip to main content

omena_query/
style.rs

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