Skip to main content

omena_semantic/
design_tokens.rs

1//! Design-token semantic analysis for CSS custom properties.
2//!
3//! The module ranks declarations, records workspace-scoped candidates, and
4//! exposes capability signals for cross-file design-token hover, completion,
5//! diagnostics, and cascade-aware resolution.
6
7use omena_cascade::{
8    CascadeKey, CascadeLevel, LayerRank, ModuleRank, SelectorMatchVerdict, Specificity,
9    select_cascade_winner, selector_context_witness, selector_context_witness_for_declaration,
10};
11use omena_syntax::css_keyword;
12use serde::Serialize;
13use std::collections::{BTreeMap, BTreeSet};
14
15use crate::{
16    ParserBoundarySyntaxFactsV0, ParserByteSpanV0, ParserIndexCustomPropertyDeclFactV0,
17    ParserIndexCustomPropertyRefFactV0, ParserRangeV0, StyleContextIndexV0, StyleSemanticFactsV0,
18};
19
20#[derive(Debug, Clone, PartialEq, Eq, Serialize)]
21#[serde(rename_all = "camelCase")]
22pub struct DesignTokenSemanticSummaryV0 {
23    pub schema_version: &'static str,
24    pub product: &'static str,
25    pub status: &'static str,
26    pub resolution_scope: &'static str,
27    pub declaration_count: usize,
28    pub reference_count: usize,
29    pub resolved_reference_count: usize,
30    pub unresolved_reference_count: usize,
31    pub selectors_with_references_count: usize,
32    pub context_signal: DesignTokenContextSignalV0,
33    pub resolution_signal: DesignTokenResolutionSignalV0,
34    pub cascade_ranking_signal: DesignTokenCascadeRankingSignalV0,
35    pub declaration_candidates: Vec<DesignTokenDeclarationCandidateV0>,
36    pub capabilities: DesignTokenSemanticCapabilitiesV0,
37    pub blocking_gaps: Vec<&'static str>,
38    pub next_priorities: Vec<&'static str>,
39}
40
41#[derive(Debug, Clone, PartialEq, Eq, Serialize)]
42#[serde(rename_all = "camelCase")]
43pub struct DesignTokenContextSignalV0 {
44    pub declaration_context_selector_count: usize,
45    pub declaration_wrapper_context_count: usize,
46    pub media_context_selector_count: usize,
47    pub supports_context_selector_count: usize,
48    pub layer_context_selector_count: usize,
49    pub wrapper_context_count: usize,
50}
51
52#[derive(Debug, Clone, PartialEq, Eq, Serialize)]
53#[serde(rename_all = "camelCase")]
54pub struct DesignTokenResolutionSignalV0 {
55    pub declaration_fact_count: usize,
56    pub reference_fact_count: usize,
57    pub source_ordered_declaration_count: usize,
58    pub source_ordered_reference_count: usize,
59    pub occurrence_resolved_reference_count: usize,
60    pub occurrence_unresolved_reference_count: usize,
61    pub workspace_declaration_fact_count: usize,
62    pub cross_file_declaration_fact_count: usize,
63    pub workspace_occurrence_resolved_reference_count: usize,
64    pub workspace_occurrence_unresolved_reference_count: usize,
65    pub context_matched_reference_count: usize,
66    pub context_unmatched_reference_count: usize,
67    pub root_declaration_count: usize,
68    pub selector_scoped_declaration_count: usize,
69    pub wrapper_scoped_declaration_count: usize,
70}
71
72#[derive(Debug, Clone, PartialEq, Eq, Serialize)]
73#[serde(rename_all = "camelCase")]
74pub struct DesignTokenCascadeRankingSignalV0 {
75    pub ranked_reference_count: usize,
76    pub unranked_reference_count: usize,
77    pub source_order_winner_declaration_count: usize,
78    pub source_order_shadowed_declaration_count: usize,
79    pub repeated_name_declaration_count: usize,
80    pub theme_context_winner_reference_count: usize,
81    pub cross_file_candidate_declaration_count: usize,
82    pub cross_file_winner_declaration_count: usize,
83    pub cross_file_shadowed_declaration_count: usize,
84    pub ranked_references: Vec<DesignTokenRankedReferenceV0>,
85}
86
87#[derive(Debug, Clone, PartialEq, Eq, Serialize)]
88#[serde(rename_all = "camelCase")]
89pub struct DesignTokenRankedReferenceV0 {
90    pub reference_name: String,
91    pub reference_source_order: usize,
92    pub winner_declaration_source_order: usize,
93    #[serde(skip_serializing_if = "Option::is_none")]
94    pub winner_declaration_file_path: Option<String>,
95    #[serde(skip_serializing_if = "Option::is_none")]
96    pub winner_declaration_range: Option<ParserRangeV0>,
97    #[serde(skip_serializing_if = "Option::is_none")]
98    pub winner_import_graph_distance: Option<usize>,
99    #[serde(skip_serializing_if = "Option::is_none")]
100    pub winner_import_graph_order: Option<usize>,
101    pub winner_declaration_layer_rank: i32,
102    pub winner_scope_proximity_status: &'static str,
103    #[serde(skip_serializing_if = "Option::is_none")]
104    pub winner_declaration_layer_name: Option<String>,
105    pub shadowed_declaration_source_orders: Vec<usize>,
106    pub candidate_declaration_count: usize,
107    pub winner_context_kind: &'static str,
108    pub cross_file_candidate_declaration_count: usize,
109    pub cross_file_shadowed_declaration_count: usize,
110}
111
112#[derive(Debug, Clone, PartialEq, Eq, Serialize)]
113#[serde(rename_all = "camelCase")]
114pub struct DesignTokenSemanticCapabilitiesV0 {
115    pub same_file_resolution_ready: bool,
116    pub wrapper_context_signal_ready: bool,
117    pub source_order_signal_ready: bool,
118    pub source_order_cascade_ranking_ready: bool,
119    pub workspace_cascade_candidate_signal_ready: bool,
120    pub occurrence_resolution_signal_ready: bool,
121    pub selector_context_resolution_ready: bool,
122    pub theme_override_context_signal_ready: bool,
123    pub cross_file_import_graph_ready: bool,
124    pub cross_package_cascade_ranking_ready: bool,
125    pub theme_override_context_ready: bool,
126}
127
128#[derive(Debug, Clone, PartialEq, Eq)]
129pub struct DesignTokenWorkspaceDeclarationFactV0 {
130    pub file_path: String,
131    pub name: String,
132    pub value: String,
133    pub source_order: usize,
134    pub import_graph_distance: Option<usize>,
135    pub import_graph_order: Option<usize>,
136    pub byte_span: ParserByteSpanV0,
137    pub range: ParserRangeV0,
138    pub selector_contexts: Vec<String>,
139    pub condition_context: Vec<String>,
140    pub layer_names: Vec<String>,
141    pub under_media: bool,
142    pub under_supports: bool,
143    pub under_layer: bool,
144}
145
146#[derive(Debug, Clone, PartialEq, Eq, Serialize)]
147#[serde(rename_all = "camelCase")]
148pub struct DesignTokenDeclarationCandidateV0 {
149    pub name: String,
150    pub value: String,
151    pub source_order: usize,
152    pub file_path: String,
153    pub range: ParserRangeV0,
154    pub selector_contexts: Vec<String>,
155    #[serde(default, skip_serializing_if = "Vec::is_empty")]
156    pub condition_context: Vec<String>,
157    pub layer_names: Vec<String>,
158    pub under_media: bool,
159    pub under_supports: bool,
160    pub under_layer: bool,
161    pub candidate_scope: &'static str,
162    #[serde(skip_serializing_if = "Option::is_none")]
163    pub import_graph_distance: Option<usize>,
164    #[serde(skip_serializing_if = "Option::is_none")]
165    pub import_graph_order: Option<usize>,
166}
167
168#[derive(Debug, Clone, Copy, PartialEq, Eq)]
169pub enum DesignTokenExternalDeclarationCandidateScopeV0 {
170    Workspace,
171    CrossFileImportGraph,
172}
173
174pub fn summarize_design_token_semantics(
175    parser_facts: &ParserBoundarySyntaxFactsV0,
176    semantic_facts: &StyleSemanticFactsV0,
177) -> DesignTokenSemanticSummaryV0 {
178    summarize_design_token_semantics_with_workspace_declarations(
179        parser_facts,
180        semantic_facts,
181        None,
182        &[],
183    )
184}
185
186pub fn summarize_design_token_semantics_with_workspace_declarations(
187    parser_facts: &ParserBoundarySyntaxFactsV0,
188    semantic_facts: &StyleSemanticFactsV0,
189    target_style_path: Option<&str>,
190    workspace_declarations: &[DesignTokenWorkspaceDeclarationFactV0],
191) -> DesignTokenSemanticSummaryV0 {
192    summarize_design_token_semantics_with_scoped_workspace_declarations(
193        parser_facts,
194        semantic_facts,
195        target_style_path,
196        workspace_declarations,
197        DesignTokenExternalDeclarationCandidateScopeV0::Workspace,
198    )
199}
200
201pub fn summarize_design_token_semantics_with_scoped_workspace_declarations(
202    parser_facts: &ParserBoundarySyntaxFactsV0,
203    semantic_facts: &StyleSemanticFactsV0,
204    target_style_path: Option<&str>,
205    workspace_declarations: &[DesignTokenWorkspaceDeclarationFactV0],
206    candidate_scope: DesignTokenExternalDeclarationCandidateScopeV0,
207) -> DesignTokenSemanticSummaryV0 {
208    let media_context_selector_count = parser_facts
209        .custom_properties
210        .selectors_with_refs_under_media_names
211        .len();
212    let supports_context_selector_count = parser_facts
213        .custom_properties
214        .selectors_with_refs_under_supports_names
215        .len();
216    let layer_context_selector_count = parser_facts
217        .custom_properties
218        .selectors_with_refs_under_layer_names
219        .len();
220    let declaration_wrapper_context_count =
221        parser_facts.custom_properties.decl_names_under_media.len()
222            + parser_facts
223                .custom_properties
224                .decl_names_under_supports
225                .len()
226            + parser_facts.custom_properties.decl_names_under_layer.len();
227    let wrapper_context_count = media_context_selector_count
228        + supports_context_selector_count
229        + layer_context_selector_count;
230    let declaration_context_selector_count =
231        parser_facts.custom_properties.decl_context_selectors.len();
232    let reference_count = semantic_facts.custom_properties.ref_names.len();
233    let declaration_count = semantic_facts.custom_properties.decl_names.len();
234    let resolution_signal = summarize_design_token_resolution_signal(
235        parser_facts,
236        target_style_path,
237        workspace_declarations,
238    );
239    let cascade_ranking_signal = summarize_design_token_cascade_ranking_signal(
240        parser_facts,
241        semantic_facts,
242        target_style_path,
243        workspace_declarations,
244    );
245
246    let external_candidate_scope_ready = candidate_scope.cross_file_import_graph_ready();
247    let status = if reference_count == 0 && declaration_count == 0 {
248        "empty"
249    } else if cascade_ranking_signal.has_workspace_signal() && external_candidate_scope_ready {
250        "cross-file-import-cascade-ranking-seed"
251    } else if cascade_ranking_signal.has_workspace_signal() {
252        "workspace-cascade-ranking-seed"
253    } else if cascade_ranking_signal.has_shadowing_signal() {
254        "same-file-cascade-ranking-seed"
255    } else if resolution_signal.occurrence_resolution_ready() {
256        "context-aware-resolution-seed"
257    } else if wrapper_context_count > 0 {
258        "context-aware-seed"
259    } else {
260        "same-file-seed"
261    };
262
263    let mut blocking_gaps = Vec::new();
264    if reference_count > 0 || declaration_count > 0 {
265        if !external_candidate_scope_ready {
266            blocking_gaps.push("crossFileImportGraph");
267        }
268        blocking_gaps.push("crossPackageCascadeRanking");
269        if !cascade_ranking_signal.theme_override_context_ready() {
270            blocking_gaps.push("themeOverrideContext");
271        }
272    }
273    if !semantic_facts
274        .custom_properties
275        .unresolved_ref_names
276        .is_empty()
277    {
278        blocking_gaps.push("unresolvedDesignTokenRefs");
279    }
280
281    let next_priorities = if reference_count == 0 && declaration_count == 0 {
282        vec!["designTokenSeed"]
283    } else {
284        let mut priorities = Vec::new();
285        if !external_candidate_scope_ready {
286            priorities.push("crossFileImportGraph");
287        }
288        priorities.push("crossPackageCascadeRanking");
289        if !cascade_ranking_signal.theme_override_context_ready() {
290            priorities.push("themeOverrideContext");
291        }
292        priorities
293    };
294    let resolution_scope = if cascade_ranking_signal.has_workspace_signal() {
295        candidate_scope.resolution_scope()
296    } else {
297        "same-file"
298    };
299    let declaration_candidates = summarize_design_token_declaration_candidates(
300        parser_facts,
301        target_style_path,
302        workspace_declarations,
303        candidate_scope,
304    );
305
306    DesignTokenSemanticSummaryV0 {
307        schema_version: "0",
308        product: "omena-semantic.design-token-semantics",
309        status,
310        resolution_scope,
311        declaration_count,
312        reference_count,
313        resolved_reference_count: semantic_facts.custom_properties.resolved_ref_names.len(),
314        unresolved_reference_count: semantic_facts.custom_properties.unresolved_ref_names.len(),
315        selectors_with_references_count: semantic_facts
316            .custom_properties
317            .selectors_with_refs_names
318            .len(),
319        context_signal: DesignTokenContextSignalV0 {
320            declaration_context_selector_count,
321            declaration_wrapper_context_count,
322            media_context_selector_count,
323            supports_context_selector_count,
324            layer_context_selector_count,
325            wrapper_context_count,
326        },
327        resolution_signal: resolution_signal.clone(),
328        cascade_ranking_signal: cascade_ranking_signal.clone(),
329        declaration_candidates,
330        capabilities: DesignTokenSemanticCapabilitiesV0 {
331            same_file_resolution_ready: declaration_count > 0 || reference_count > 0,
332            wrapper_context_signal_ready: wrapper_context_count > 0,
333            source_order_signal_ready: resolution_signal.source_order_signal_ready(),
334            source_order_cascade_ranking_ready: cascade_ranking_signal
335                .source_order_cascade_ranking_ready(),
336            workspace_cascade_candidate_signal_ready: cascade_ranking_signal.has_workspace_signal(),
337            occurrence_resolution_signal_ready: resolution_signal.occurrence_resolution_ready(),
338            selector_context_resolution_ready: resolution_signal
339                .selector_context_resolution_ready(),
340            theme_override_context_signal_ready: declaration_context_selector_count > 0
341                || declaration_wrapper_context_count > 0,
342            cross_file_import_graph_ready: external_candidate_scope_ready,
343            cross_package_cascade_ranking_ready: false,
344            theme_override_context_ready: cascade_ranking_signal.theme_override_context_ready(),
345        },
346        blocking_gaps,
347        next_priorities,
348    }
349}
350
351fn summarize_design_token_declaration_candidates(
352    parser_facts: &ParserBoundarySyntaxFactsV0,
353    target_style_path: Option<&str>,
354    workspace_declarations: &[DesignTokenWorkspaceDeclarationFactV0],
355    candidate_scope: DesignTokenExternalDeclarationCandidateScopeV0,
356) -> Vec<DesignTokenDeclarationCandidateV0> {
357    let mut candidates = Vec::new();
358    if let Some(file_path) = target_style_path {
359        candidates.extend(
360            parser_facts
361                .custom_properties
362                .decl_facts
363                .iter()
364                .map(|declaration| DesignTokenDeclarationCandidateV0 {
365                    name: declaration.name.clone(),
366                    value: declaration.value.clone(),
367                    source_order: declaration.source_order,
368                    file_path: file_path.to_string(),
369                    range: declaration.range,
370                    selector_contexts: declaration.selector_contexts.clone(),
371                    condition_context: declaration.condition_context.clone(),
372                    layer_names: declaration.layer_names.clone(),
373                    under_media: declaration.under_media,
374                    under_supports: declaration.under_supports,
375                    under_layer: declaration.under_layer,
376                    candidate_scope: "same-file",
377                    import_graph_distance: None,
378                    import_graph_order: None,
379                }),
380        );
381    }
382    candidates.extend(workspace_declarations.iter().map(|declaration| {
383        DesignTokenDeclarationCandidateV0 {
384            name: declaration.name.clone(),
385            value: declaration.value.clone(),
386            source_order: declaration.source_order,
387            file_path: declaration.file_path.clone(),
388            range: declaration.range,
389            selector_contexts: declaration.selector_contexts.clone(),
390            condition_context: declaration.condition_context.clone(),
391            layer_names: declaration.layer_names.clone(),
392            under_media: declaration.under_media,
393            under_supports: declaration.under_supports,
394            under_layer: declaration.under_layer,
395            candidate_scope: candidate_scope.resolution_scope(),
396            import_graph_distance: declaration.import_graph_distance,
397            import_graph_order: declaration.import_graph_order,
398        }
399    }));
400    candidates.sort_by(|left, right| {
401        left.file_path
402            .cmp(&right.file_path)
403            .then_with(|| left.source_order.cmp(&right.source_order))
404            .then_with(|| left.name.cmp(&right.name))
405    });
406    candidates.dedup_by(|left, right| {
407        left.file_path == right.file_path
408            && left.source_order == right.source_order
409            && left.name == right.name
410            && left.range == right.range
411    });
412    candidates
413}
414
415pub fn collect_design_token_workspace_declarations(
416    style_path: &str,
417    parser_facts: &ParserBoundarySyntaxFactsV0,
418) -> Vec<DesignTokenWorkspaceDeclarationFactV0> {
419    parser_facts
420        .custom_properties
421        .decl_facts
422        .iter()
423        .map(|declaration| DesignTokenWorkspaceDeclarationFactV0 {
424            file_path: style_path.to_string(),
425            name: declaration.name.clone(),
426            value: declaration.value.clone(),
427            source_order: declaration.source_order,
428            import_graph_distance: None,
429            import_graph_order: None,
430            byte_span: declaration.byte_span,
431            range: declaration.range,
432            selector_contexts: declaration.selector_contexts.clone(),
433            condition_context: declaration.condition_context.clone(),
434            layer_names: declaration.layer_names.clone(),
435            under_media: declaration.under_media,
436            under_supports: declaration.under_supports,
437            under_layer: declaration.under_layer,
438        })
439        .collect()
440}
441
442fn summarize_design_token_cascade_ranking_signal(
443    parser_facts: &ParserBoundarySyntaxFactsV0,
444    semantic_facts: &StyleSemanticFactsV0,
445    target_style_path: Option<&str>,
446    workspace_declarations: &[DesignTokenWorkspaceDeclarationFactV0],
447) -> DesignTokenCascadeRankingSignalV0 {
448    let custom_properties = &parser_facts.custom_properties;
449    let cascade_context =
450        DesignTokenCascadeContext::from_style_context_index(&semantic_facts.context_index);
451    let mut declaration_name_counts = BTreeMap::<&str, usize>::new();
452    let mut winner_declarations = BTreeSet::<(String, usize)>::new();
453    let mut shadowed_declarations = BTreeSet::<(String, usize)>::new();
454    let mut ranked_reference_count = 0;
455    let mut unranked_reference_count = 0;
456    let mut cross_file_candidate_declaration_count = 0;
457    let mut cross_file_winner_declaration_count = 0;
458    let mut cross_file_shadowed_declaration_count = 0;
459    let mut theme_context_winner_reference_count = 0;
460    let mut ranked_references = Vec::new();
461
462    for declaration in &custom_properties.decl_facts {
463        *declaration_name_counts
464            .entry(declaration.name.as_str())
465            .or_insert(0) += 1;
466    }
467
468    for reference in &custom_properties.ref_facts {
469        let local_candidates = custom_properties
470            .decl_facts
471            .iter()
472            .filter(|declaration| custom_property_context_matches(declaration, reference))
473            .collect::<Vec<_>>();
474        let workspace_candidates = workspace_declarations
475            .iter()
476            .filter(|declaration| {
477                target_style_path.is_none_or(|target| declaration.file_path != target)
478                    && custom_property_workspace_context_matches(declaration, reference)
479            })
480            .collect::<Vec<_>>();
481
482        let local_winner = select_cascade_winner(
483            local_candidates
484                .iter()
485                .copied()
486                .map(DesignTokenCandidateDeclaration::Local),
487            |candidate| candidate.cascade_key(reference, None, &cascade_context),
488        )
489        .map(|(winner, _)| winner);
490        let workspace_file_ranks = summarize_workspace_candidate_file_ranks(&workspace_candidates);
491        let workspace_winner = select_cascade_winner(
492            workspace_candidates
493                .iter()
494                .copied()
495                .map(DesignTokenCandidateDeclaration::Workspace),
496            |candidate| {
497                candidate.cascade_key(reference, Some(&workspace_file_ranks), &cascade_context)
498            },
499        )
500        .map(|(winner, _)| winner);
501        let winner = local_winner.or(workspace_winner);
502
503        let Some(winner) = winner else {
504            unranked_reference_count += 1;
505            continue;
506        };
507
508        ranked_reference_count += 1;
509        let candidate_declaration_count = local_candidates.len() + workspace_candidates.len();
510        let reference_cross_file_candidate_declaration_count = workspace_candidates.len();
511        cross_file_candidate_declaration_count += reference_cross_file_candidate_declaration_count;
512        let mut shadowed_declaration_source_orders = Vec::new();
513        for candidate in local_candidates {
514            if winner.is_local_source_order(candidate.source_order) {
515                winner_declarations.insert(custom_property_declaration_key(candidate));
516            } else {
517                shadowed_declaration_source_orders.push(candidate.source_order);
518                shadowed_declarations.insert(custom_property_declaration_key(candidate));
519            }
520        }
521        let reference_cross_file_shadowed_declaration_count = workspace_candidates
522            .iter()
523            .filter(|candidate| !winner.is_workspace(candidate))
524            .count();
525        cross_file_shadowed_declaration_count += reference_cross_file_shadowed_declaration_count;
526        if winner.is_workspace_winner() {
527            cross_file_winner_declaration_count += 1;
528        }
529        if winner.is_theme_context_winner(reference) {
530            theme_context_winner_reference_count += 1;
531        }
532        shadowed_declaration_source_orders.sort_unstable();
533        ranked_references.push(DesignTokenRankedReferenceV0 {
534            reference_name: reference.name.clone(),
535            reference_source_order: reference.source_order,
536            winner_declaration_source_order: winner.source_order(),
537            winner_declaration_file_path: winner.file_path().map(ToString::to_string),
538            winner_declaration_range: winner.range(),
539            winner_import_graph_distance: winner.import_graph_distance(),
540            winner_import_graph_order: winner.import_graph_order(),
541            winner_declaration_layer_rank: winner.layer_rank(&cascade_context).0,
542            winner_scope_proximity_status: "legacySelectorContextFallback",
543            winner_declaration_layer_name: winner.layer_name(&cascade_context),
544            shadowed_declaration_source_orders,
545            candidate_declaration_count,
546            winner_context_kind: winner.context_kind(reference),
547            cross_file_candidate_declaration_count:
548                reference_cross_file_candidate_declaration_count,
549            cross_file_shadowed_declaration_count: reference_cross_file_shadowed_declaration_count,
550        });
551    }
552
553    DesignTokenCascadeRankingSignalV0 {
554        ranked_reference_count,
555        unranked_reference_count,
556        source_order_winner_declaration_count: winner_declarations.len(),
557        source_order_shadowed_declaration_count: shadowed_declarations.len(),
558        repeated_name_declaration_count: custom_properties
559            .decl_facts
560            .iter()
561            .filter(|declaration| {
562                declaration_name_counts
563                    .get(declaration.name.as_str())
564                    .is_some_and(|count| *count > 1)
565            })
566            .count(),
567        theme_context_winner_reference_count,
568        cross_file_candidate_declaration_count,
569        cross_file_winner_declaration_count,
570        cross_file_shadowed_declaration_count,
571        ranked_references,
572    }
573}
574
575fn summarize_design_token_resolution_signal(
576    parser_facts: &ParserBoundarySyntaxFactsV0,
577    target_style_path: Option<&str>,
578    workspace_declarations: &[DesignTokenWorkspaceDeclarationFactV0],
579) -> DesignTokenResolutionSignalV0 {
580    let custom_properties = &parser_facts.custom_properties;
581    let mut occurrence_resolved_reference_count = 0;
582    let mut occurrence_unresolved_reference_count = 0;
583    let mut workspace_occurrence_resolved_reference_count = 0;
584    let mut workspace_occurrence_unresolved_reference_count = 0;
585    let cross_file_declaration_fact_count = workspace_declarations
586        .iter()
587        .filter(|declaration| {
588            target_style_path.is_none_or(|target| declaration.file_path != target)
589        })
590        .count();
591
592    for reference in &custom_properties.ref_facts {
593        let has_same_file_match = custom_properties
594            .decl_facts
595            .iter()
596            .any(|declaration| custom_property_context_matches(declaration, reference));
597        let has_workspace_match = has_same_file_match
598            || workspace_declarations.iter().any(|declaration| {
599                target_style_path.is_none_or(|target| declaration.file_path != target)
600                    && custom_property_workspace_context_matches(declaration, reference)
601            });
602
603        if has_same_file_match {
604            occurrence_resolved_reference_count += 1;
605        } else {
606            occurrence_unresolved_reference_count += 1;
607        }
608        if has_workspace_match {
609            workspace_occurrence_resolved_reference_count += 1;
610        } else {
611            workspace_occurrence_unresolved_reference_count += 1;
612        }
613    }
614
615    DesignTokenResolutionSignalV0 {
616        declaration_fact_count: custom_properties.decl_facts.len(),
617        reference_fact_count: custom_properties.ref_facts.len(),
618        source_ordered_declaration_count: custom_properties.decl_facts.len(),
619        source_ordered_reference_count: custom_properties.ref_facts.len(),
620        occurrence_resolved_reference_count,
621        occurrence_unresolved_reference_count,
622        workspace_declaration_fact_count: custom_properties.decl_facts.len()
623            + cross_file_declaration_fact_count,
624        cross_file_declaration_fact_count,
625        workspace_occurrence_resolved_reference_count,
626        workspace_occurrence_unresolved_reference_count,
627        context_matched_reference_count: occurrence_resolved_reference_count,
628        context_unmatched_reference_count: occurrence_unresolved_reference_count,
629        root_declaration_count: custom_properties
630            .decl_facts
631            .iter()
632            .filter(|declaration| {
633                declaration
634                    .selector_contexts
635                    .iter()
636                    .any(|selector| css_keyword(selector).equals(":root"))
637            })
638            .count(),
639        selector_scoped_declaration_count: custom_properties
640            .decl_facts
641            .iter()
642            .filter(|declaration| {
643                declaration
644                    .selector_contexts
645                    .iter()
646                    .any(|selector| !css_keyword(selector).equals(":root"))
647            })
648            .count(),
649        wrapper_scoped_declaration_count: custom_properties
650            .decl_facts
651            .iter()
652            .filter(|declaration| {
653                declaration.under_media || declaration.under_supports || declaration.under_layer
654            })
655            .count(),
656    }
657}
658
659impl DesignTokenResolutionSignalV0 {
660    fn occurrence_resolution_ready(&self) -> bool {
661        self.declaration_fact_count > 0 || self.reference_fact_count > 0
662    }
663
664    fn source_order_signal_ready(&self) -> bool {
665        self.source_ordered_declaration_count > 0 || self.source_ordered_reference_count > 0
666    }
667
668    fn selector_context_resolution_ready(&self) -> bool {
669        self.occurrence_resolution_ready()
670            && (self.root_declaration_count > 0 || self.selector_scoped_declaration_count > 0)
671    }
672}
673
674impl DesignTokenCascadeRankingSignalV0 {
675    fn source_order_cascade_ranking_ready(&self) -> bool {
676        self.ranked_reference_count > 0
677    }
678
679    fn has_shadowing_signal(&self) -> bool {
680        self.source_order_shadowed_declaration_count > 0
681    }
682
683    fn has_workspace_signal(&self) -> bool {
684        self.cross_file_candidate_declaration_count > 0
685    }
686
687    fn theme_override_context_ready(&self) -> bool {
688        self.theme_context_winner_reference_count > 0
689    }
690}
691
692impl DesignTokenExternalDeclarationCandidateScopeV0 {
693    fn cross_file_import_graph_ready(self) -> bool {
694        matches!(
695            self,
696            DesignTokenExternalDeclarationCandidateScopeV0::CrossFileImportGraph
697        )
698    }
699
700    fn resolution_scope(self) -> &'static str {
701        match self {
702            DesignTokenExternalDeclarationCandidateScopeV0::Workspace => "workspace-candidate",
703            DesignTokenExternalDeclarationCandidateScopeV0::CrossFileImportGraph => {
704                "cross-file-import-candidate"
705            }
706        }
707    }
708}
709
710#[derive(Clone, Copy)]
711enum DesignTokenCandidateDeclaration<'a> {
712    Local(&'a ParserIndexCustomPropertyDeclFactV0),
713    Workspace(&'a DesignTokenWorkspaceDeclarationFactV0),
714}
715
716#[derive(Debug, Clone, PartialEq, Eq)]
717struct DesignTokenCascadeContext {
718    layer_name_ranks: BTreeMap<String, i32>,
719    layer_name_depths: BTreeMap<String, usize>,
720    layer_ranks_by_selector: BTreeMap<String, i32>,
721    layer_names_by_selector: BTreeMap<String, String>,
722    unlayered_rank: i32,
723}
724
725impl DesignTokenCascadeContext {
726    fn from_style_context_index(index: &StyleContextIndexV0) -> Self {
727        let mut layer_name_ranks = BTreeMap::<String, i32>::new();
728        let mut layer_name_depths = BTreeMap::<String, usize>::new();
729        let mut local_name_counts = BTreeMap::<&str, usize>::new();
730        for layer in &index.layer_index.order_nodes {
731            *local_name_counts
732                .entry(layer.local_name.as_str())
733                .or_default() += 1;
734            layer_name_ranks.insert(
735                layer.canonical_name.clone(),
736                layer.cascade_rank.min(i32::MAX as usize) as i32,
737            );
738            layer_name_depths.insert(layer.canonical_name.clone(), layer.nesting_depth);
739        }
740        for layer in &index.layer_index.order_nodes {
741            if local_name_counts.get(layer.local_name.as_str()) == Some(&1) {
742                layer_name_ranks.insert(
743                    layer.local_name.clone(),
744                    layer.cascade_rank.min(i32::MAX as usize) as i32,
745                );
746                layer_name_depths.insert(layer.local_name.clone(), layer.nesting_depth);
747            }
748        }
749
750        let mut block_layer_ranks = BTreeMap::<String, (usize, i32, String)>::new();
751        for binding in &index.layer_index.block_bindings {
752            block_layer_ranks.insert(
753                binding.context_id.clone(),
754                (
755                    binding.nesting_depth,
756                    binding.cascade_rank.min(i32::MAX as usize) as i32,
757                    binding.canonical_name.clone(),
758                ),
759            );
760        }
761
762        let mut selector_layers = BTreeMap::<String, (usize, i32, String)>::new();
763        for membership in &index.layer_index.selector_memberships {
764            let Some(candidate) = block_layer_ranks.get(&membership.context_id) else {
765                continue;
766            };
767            let entry = selector_layers
768                .entry(membership.selector_name.clone())
769                .or_insert_with(|| candidate.clone());
770            if candidate.0 > entry.0 || (candidate.0 == entry.0 && candidate.1 > entry.1) {
771                *entry = candidate.clone();
772            }
773        }
774        let layer_ranks_by_selector = selector_layers
775            .iter()
776            .map(|(selector, (_, rank, _))| (selector.clone(), *rank))
777            .collect();
778        let layer_names_by_selector = selector_layers
779            .into_iter()
780            .map(|(selector, (_, _, name))| (selector, name))
781            .collect();
782
783        let unlayered_rank = index
784            .layer_index
785            .order_nodes
786            .len()
787            .saturating_add(index.layer_index.anonymous_layer_block_count)
788            .saturating_add(1)
789            .min(i32::MAX as usize) as i32;
790
791        Self {
792            layer_name_ranks,
793            layer_name_depths,
794            layer_ranks_by_selector,
795            layer_names_by_selector,
796            unlayered_rank,
797        }
798    }
799
800    fn layer_rank_for(
801        &self,
802        layer_names: &[String],
803        selector_contexts: &[String],
804        under_layer: bool,
805    ) -> LayerRank {
806        if !under_layer {
807            return LayerRank(self.unlayered_rank);
808        }
809        let canonical_path = layer_names.join(".");
810        if let Some(rank) = self.layer_name_ranks.get(canonical_path.as_str()) {
811            return LayerRank(*rank);
812        }
813        if let Some((rank, _)) = layer_names
814            .iter()
815            .filter_map(|name| {
816                Some((
817                    self.layer_name_ranks.get(name).copied()?,
818                    self.layer_name_depths.get(name).copied().unwrap_or(0),
819                ))
820            })
821            .max_by_key(|(_, depth)| *depth)
822        {
823            return LayerRank(rank);
824        }
825        selector_contexts
826            .iter()
827            .filter_map(|selector| {
828                self.layer_ranks_by_selector
829                    .get(normalized_selector(selector))
830            })
831            .copied()
832            .max()
833            .map(LayerRank)
834            .unwrap_or(LayerRank(0))
835    }
836
837    fn layer_name_for(
838        &self,
839        layer_names: &[String],
840        selector_contexts: &[String],
841        under_layer: bool,
842    ) -> Option<String> {
843        if !under_layer {
844            return None;
845        }
846        let canonical_path = layer_names.join(".");
847        if self.layer_name_ranks.contains_key(canonical_path.as_str()) {
848            return Some(canonical_path);
849        }
850        if let Some(name) = layer_names
851            .iter()
852            .filter(|name| self.layer_name_ranks.contains_key(*name))
853            .max_by_key(|name| self.layer_name_depths.get(*name).copied().unwrap_or(0))
854        {
855            return Some(name.clone());
856        }
857        selector_contexts.iter().find_map(|selector| {
858            self.layer_names_by_selector
859                .get(normalized_selector(selector))
860                .cloned()
861        })
862    }
863}
864
865impl DesignTokenCandidateDeclaration<'_> {
866    fn cascade_key(
867        &self,
868        reference: &ParserIndexCustomPropertyRefFactV0,
869        workspace_file_ranks: Option<&BTreeMap<&str, usize>>,
870        cascade_context: &DesignTokenCascadeContext,
871    ) -> CascadeKey {
872        let scope_proximity = cascade_scope_proximity_fallback_for_selector_context_rank(
873            self.context_rank(reference),
874        );
875        match self {
876            DesignTokenCandidateDeclaration::Local(declaration) => CascadeKey::new(
877                CascadeLevel::AuthorNormal,
878                cascade_context.layer_rank_for(
879                    &declaration.layer_names,
880                    &declaration.selector_contexts,
881                    declaration.under_layer,
882                ),
883                scope_proximity,
884                Specificity::ZERO,
885                ModuleRank::ZERO,
886                cascade_u32_rank(declaration.source_order),
887            ),
888            DesignTokenCandidateDeclaration::Workspace(declaration) => {
889                let file_rank = workspace_file_ranks
890                    .and_then(|ranks| ranks.get(declaration.file_path.as_str()).copied())
891                    .unwrap_or(usize::MAX);
892                CascadeKey::new(
893                    CascadeLevel::AuthorNormal,
894                    cascade_context.layer_rank_for(
895                        &declaration.layer_names,
896                        &declaration.selector_contexts,
897                        declaration.under_layer,
898                    ),
899                    scope_proximity,
900                    Specificity::ZERO,
901                    ModuleRank::new(
902                        cascade_inverse_rank(
903                            declaration.import_graph_distance.unwrap_or(usize::MAX),
904                        ),
905                        cascade_inverse_rank(declaration.import_graph_order.unwrap_or(usize::MAX)),
906                        cascade_inverse_rank(file_rank),
907                    ),
908                    cascade_u32_rank(declaration.source_order),
909                )
910            }
911        }
912    }
913
914    fn source_order(&self) -> usize {
915        match self {
916            DesignTokenCandidateDeclaration::Local(declaration) => declaration.source_order,
917            DesignTokenCandidateDeclaration::Workspace(declaration) => declaration.source_order,
918        }
919    }
920
921    fn file_path(&self) -> Option<&str> {
922        match self {
923            DesignTokenCandidateDeclaration::Local(_) => None,
924            DesignTokenCandidateDeclaration::Workspace(declaration) => {
925                Some(declaration.file_path.as_str())
926            }
927        }
928    }
929
930    fn range(&self) -> Option<ParserRangeV0> {
931        match self {
932            DesignTokenCandidateDeclaration::Local(_) => None,
933            DesignTokenCandidateDeclaration::Workspace(declaration) => Some(declaration.range),
934        }
935    }
936
937    fn import_graph_distance(&self) -> Option<usize> {
938        match self {
939            DesignTokenCandidateDeclaration::Local(_) => None,
940            DesignTokenCandidateDeclaration::Workspace(declaration) => {
941                declaration.import_graph_distance
942            }
943        }
944    }
945
946    fn import_graph_order(&self) -> Option<usize> {
947        match self {
948            DesignTokenCandidateDeclaration::Local(_) => None,
949            DesignTokenCandidateDeclaration::Workspace(declaration) => {
950                declaration.import_graph_order
951            }
952        }
953    }
954
955    fn is_local_source_order(&self, source_order: usize) -> bool {
956        matches!(
957            self,
958            DesignTokenCandidateDeclaration::Local(declaration)
959                if declaration.source_order == source_order
960        )
961    }
962
963    fn is_workspace(&self, declaration: &DesignTokenWorkspaceDeclarationFactV0) -> bool {
964        matches!(
965            self,
966            DesignTokenCandidateDeclaration::Workspace(winner)
967                if winner.file_path == declaration.file_path
968                    && winner.source_order == declaration.source_order
969                    && winner.name == declaration.name
970        )
971    }
972
973    fn is_workspace_winner(&self) -> bool {
974        matches!(self, DesignTokenCandidateDeclaration::Workspace(_))
975    }
976
977    fn layer_rank(&self, cascade_context: &DesignTokenCascadeContext) -> LayerRank {
978        match self {
979            DesignTokenCandidateDeclaration::Local(declaration) => cascade_context.layer_rank_for(
980                &declaration.layer_names,
981                &declaration.selector_contexts,
982                declaration.under_layer,
983            ),
984            DesignTokenCandidateDeclaration::Workspace(declaration) => cascade_context
985                .layer_rank_for(
986                    &declaration.layer_names,
987                    &declaration.selector_contexts,
988                    declaration.under_layer,
989                ),
990        }
991    }
992
993    fn layer_name(&self, cascade_context: &DesignTokenCascadeContext) -> Option<String> {
994        match self {
995            DesignTokenCandidateDeclaration::Local(declaration) => cascade_context.layer_name_for(
996                &declaration.layer_names,
997                &declaration.selector_contexts,
998                declaration.under_layer,
999            ),
1000            DesignTokenCandidateDeclaration::Workspace(declaration) => cascade_context
1001                .layer_name_for(
1002                    &declaration.layer_names,
1003                    &declaration.selector_contexts,
1004                    declaration.under_layer,
1005                ),
1006        }
1007    }
1008
1009    fn is_theme_context_winner(&self, reference: &ParserIndexCustomPropertyRefFactV0) -> bool {
1010        self.context_rank(reference) >= 2
1011    }
1012
1013    fn context_rank(&self, reference: &ParserIndexCustomPropertyRefFactV0) -> usize {
1014        match self {
1015            DesignTokenCandidateDeclaration::Local(declaration) => {
1016                custom_property_declaration_context_rank(&declaration.selector_contexts, reference)
1017            }
1018            DesignTokenCandidateDeclaration::Workspace(declaration) => {
1019                custom_property_declaration_context_rank(&declaration.selector_contexts, reference)
1020            }
1021        }
1022    }
1023
1024    fn context_kind(&self, reference: &ParserIndexCustomPropertyRefFactV0) -> &'static str {
1025        match self.context_rank(reference) {
1026            2.. => "selector",
1027            1 => "root",
1028            _ => "global",
1029        }
1030    }
1031}
1032
1033fn custom_property_declaration_key(
1034    declaration: &ParserIndexCustomPropertyDeclFactV0,
1035) -> (String, usize) {
1036    (declaration.name.clone(), declaration.source_order)
1037}
1038
1039fn custom_property_context_matches(
1040    declaration: &ParserIndexCustomPropertyDeclFactV0,
1041    reference: &ParserIndexCustomPropertyRefFactV0,
1042) -> bool {
1043    if declaration.name != reference.name {
1044        return false;
1045    }
1046    if declaration.under_media && !reference.under_media {
1047        return false;
1048    }
1049    if declaration.under_supports && !reference.under_supports {
1050        return false;
1051    }
1052    if !condition_context_applies(&declaration.condition_context, &reference.condition_context) {
1053        return false;
1054    }
1055    if declaration.selector_contexts.is_empty() {
1056        return true;
1057    }
1058    declaration
1059        .selector_contexts
1060        .iter()
1061        .any(|selector| custom_property_selector_context_matches(selector, reference))
1062}
1063
1064fn custom_property_workspace_context_matches(
1065    declaration: &DesignTokenWorkspaceDeclarationFactV0,
1066    reference: &ParserIndexCustomPropertyRefFactV0,
1067) -> bool {
1068    if declaration.name != reference.name {
1069        return false;
1070    }
1071    if declaration.under_media && !reference.under_media {
1072        return false;
1073    }
1074    if declaration.under_supports && !reference.under_supports {
1075        return false;
1076    }
1077    if !condition_context_applies(&declaration.condition_context, &reference.condition_context) {
1078        return false;
1079    }
1080    if declaration.selector_contexts.is_empty() {
1081        return true;
1082    }
1083    declaration
1084        .selector_contexts
1085        .iter()
1086        .any(|selector| custom_property_selector_context_matches(selector, reference))
1087}
1088
1089fn condition_context_applies(declaration_context: &[String], reference_context: &[String]) -> bool {
1090    declaration_context
1091        .iter()
1092        .all(|condition| reference_context.iter().any(|value| value == condition))
1093}
1094
1095fn custom_property_selector_context_matches(
1096    declaration_selector: &str,
1097    reference: &ParserIndexCustomPropertyRefFactV0,
1098) -> bool {
1099    !matches!(
1100        selector_context_witness_for_declaration(
1101            declaration_selector,
1102            &reference.selector_contexts
1103        )
1104        .verdict,
1105        SelectorMatchVerdict::No
1106    )
1107}
1108
1109fn custom_property_declaration_context_rank(
1110    declaration_selectors: &[String],
1111    reference: &ParserIndexCustomPropertyRefFactV0,
1112) -> usize {
1113    selector_context_witness(declaration_selectors, &reference.selector_contexts).rank
1114}
1115
1116fn summarize_workspace_candidate_file_ranks<'a>(
1117    workspace_candidates: &[&'a DesignTokenWorkspaceDeclarationFactV0],
1118) -> BTreeMap<&'a str, usize> {
1119    workspace_candidates
1120        .iter()
1121        .map(|candidate| candidate.file_path.as_str())
1122        .collect::<BTreeSet<_>>()
1123        .into_iter()
1124        .enumerate()
1125        .map(|(rank, file_path)| (file_path, rank))
1126        .collect()
1127}
1128
1129fn cascade_scope_proximity_fallback_for_selector_context_rank(context_rank: usize) -> u32 {
1130    match context_rank {
1131        2.. => 0,
1132        1 => 1,
1133        _ => 2,
1134    }
1135}
1136
1137fn cascade_u32_rank(rank: usize) -> u32 {
1138    rank.min(u32::MAX as usize) as u32
1139}
1140
1141fn cascade_inverse_rank(rank: usize) -> u32 {
1142    u32::MAX - cascade_u32_rank(rank)
1143}
1144
1145fn normalized_selector(selector: &str) -> &str {
1146    selector.trim().trim_start_matches('.')
1147}