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