1use 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 pub winner_declaration_layer_rank: i32,
104 pub winner_scope_proximity_status: &'static str,
105 pub winner_importance_status: &'static str,
107 pub winner_source_order_status: &'static str,
109 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 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 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}