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