1use serde::{Deserialize, Serialize};
4use std::collections::BTreeSet;
5use std::sync::OnceLock;
6
7use crate::{
8 CascadeLevel,
9 axis_order::{CascadeKeyAxisV0, cascade_key_axis_order_v0},
10};
11
12#[derive(
13 Debug, Clone, Copy, Default, PartialEq, Eq, PartialOrd, Ord, Hash, Deserialize, Serialize,
14)]
15#[serde(rename_all = "camelCase")]
16pub enum CascadeOriginV0 {
17 UserAgent,
18 User,
19 #[default]
20 Author,
21 Inline,
22}
23
24impl CascadeOriginV0 {
25 pub const fn is_author(&self) -> bool {
26 matches!(self, Self::Author)
27 }
28}
29
30#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize)]
31#[serde(rename_all = "camelCase")]
32pub struct CascadeOriginDriverV0 {
33 pub origin: CascadeOriginV0,
34 pub important: bool,
35 pub level: CascadeLevel,
36}
37
38#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Deserialize, Serialize)]
39#[serde(rename_all = "camelCase")]
40pub enum CascadeWinnerAxisV0 {
41 CascadeLevel,
42 LayerRank,
43 ScopeProximity,
44 Specificity,
45 SourceOrder,
46}
47
48#[derive(Debug, Clone, Copy, PartialEq, Eq, Deserialize, Serialize)]
49#[serde(rename_all = "camelCase")]
50#[non_exhaustive]
51pub enum CascadeAxisNamedDriverV0 {
53 LegacySelectorContextFallback,
55}
56
57#[derive(Debug, Clone, Copy, PartialEq, Eq, Deserialize, Serialize)]
58#[serde(rename_all = "camelCase")]
59#[non_exhaustive]
60pub enum CascadeAxisOutOfFragmentReasonV0 {
62 ShadowTreeEncapsulationContextUnmodeled,
63}
64
65#[derive(Debug, Clone, Copy, PartialEq, Eq, Deserialize, Serialize)]
66#[serde(tag = "status", rename_all = "camelCase")]
67#[non_exhaustive]
68pub enum CascadeAxisReachStatusV0 {
70 Modeled,
71 NotReachedByProduct {
72 #[serde(rename = "namedDriver")]
73 named_driver: CascadeAxisNamedDriverV0,
74 },
75 OutOfFragment {
76 reason: CascadeAxisOutOfFragmentReasonV0,
77 },
78}
79
80#[derive(Debug, Clone, Copy, PartialEq, Eq, Deserialize, Serialize)]
81#[serde(rename_all = "camelCase")]
82#[non_exhaustive]
83pub struct CascadeAxisReachDisclosureV0 {
86 pub origin_and_importance: CascadeAxisReachStatusV0,
87 pub encapsulation_context: CascadeAxisReachStatusV0,
88 pub style_attribute: CascadeAxisReachStatusV0,
89 pub layers: CascadeAxisReachStatusV0,
90 pub specificity: CascadeAxisReachStatusV0,
91 pub scope_proximity: CascadeAxisReachStatusV0,
92 pub order_of_appearance: CascadeAxisReachStatusV0,
93}
94
95#[derive(Debug, Clone, PartialEq, Eq, Deserialize)]
96#[serde(rename_all = "camelCase")]
97struct CascadeDriverCensusV0 {
98 schema_version: String,
99 product: String,
100 levels: Vec<CascadeDriverLevelV0>,
101 winner_axes: Vec<CascadeDriverAxisV0>,
102 cascade_key_producers: Vec<CascadeKeyProducerV0>,
103 spec_axis_reach: CascadeAxisReachDisclosureV0,
104}
105
106#[derive(Debug, Clone, PartialEq, Eq, Deserialize)]
107#[serde(rename_all = "camelCase")]
108struct CascadeDriverLevelV0 {
109 level: String,
110 status: String,
111 driver_inputs: Vec<String>,
112 #[serde(default)]
113 follow_up: Option<String>,
114}
115
116#[derive(Debug, Clone, PartialEq, Eq, Deserialize)]
117#[serde(rename_all = "camelCase")]
118struct CascadeDriverAxisV0 {
119 axis: CascadeWinnerAxisV0,
120 status: CascadeDriverAxisStatusV0,
121 #[serde(default)]
122 named_driver: Option<CascadeAxisNamedDriverV0>,
123}
124
125#[derive(Debug, Clone, Copy, PartialEq, Eq, Deserialize)]
126#[serde(rename_all = "camelCase")]
127enum CascadeDriverAxisStatusV0 {
128 Driven,
129 AutomaticProductDriver,
130}
131
132impl CascadeDriverAxisStatusV0 {
133 const fn is_driven(self) -> bool {
134 matches!(self, Self::Driven | Self::AutomaticProductDriver)
135 }
136}
137
138#[derive(Debug, Clone, PartialEq, Eq, Deserialize)]
139#[serde(rename_all = "camelCase")]
140struct CascadeKeyProducerV0 {
141 path: String,
142 symbol: String,
143 occurrence: u32,
144 disposition: CascadeKeyProducerDispositionV0,
145 scope_proximity_source: CascadeScopeProximitySourceV0,
146}
147
148#[derive(Debug, Clone, Copy, PartialEq, Eq, Deserialize)]
149#[serde(rename_all = "camelCase")]
150enum CascadeKeyProducerDispositionV0 {
151 AutomaticProductDerived,
152 CallerSuppliedBoundary,
153 Conformance,
154 Generated,
155 Fixture,
156}
157
158#[derive(Debug, Clone, Copy, PartialEq, Eq, Deserialize)]
159#[serde(rename_all = "camelCase")]
160enum CascadeScopeProximitySourceV0 {
161 ConstantZero,
162 LegacySelectorContextFallback,
163 CallerSupplied,
164 GeneratedValue,
165}
166
167const CASCADE_DRIVER_CENSUS_JSON: &str = include_str!("../data/cascade-driver-census.json");
168static CASCADE_DRIVER_CENSUS: OnceLock<Result<CascadeDriverCensusV0, String>> = OnceLock::new();
169
170fn cascade_driver_census_v0() -> Option<&'static CascadeDriverCensusV0> {
171 CASCADE_DRIVER_CENSUS
172 .get_or_init(|| {
173 serde_json::from_str(CASCADE_DRIVER_CENSUS_JSON).map_err(|error| error.to_string())
174 })
175 .as_ref()
176 .ok()
177}
178
179pub fn cascade_driven_levels_v0() -> Vec<CascadeLevel> {
180 cascade_driver_census_v0()
181 .into_iter()
182 .flat_map(|census| census.levels.iter())
183 .filter(|entry| entry.status == "driven")
184 .filter_map(|entry| cascade_level_from_name_v0(entry.level.as_str()))
185 .collect()
186}
187
188pub fn cascade_driven_winner_axes_v0() -> Vec<CascadeWinnerAxisV0> {
189 cascade_driver_census_v0()
190 .into_iter()
191 .flat_map(|census| census.winner_axes.iter())
192 .filter(|entry| entry.status.is_driven())
193 .map(|entry| entry.axis)
194 .collect()
195}
196
197pub fn cascade_driver_census_is_consistent_v0() -> bool {
198 let Some(census) = cascade_driver_census_v0() else {
199 return false;
200 };
201 cascade_driver_census_payload_is_consistent_v0(census)
202}
203
204pub fn summarize_cascade_axis_reach_v0() -> Option<CascadeAxisReachDisclosureV0> {
207 summarize_cascade_axis_reach_from_census_v0(cascade_driver_census_v0()?)
208}
209
210fn summarize_cascade_axis_reach_from_census_v0(
211 census: &CascadeDriverCensusV0,
212) -> Option<CascadeAxisReachDisclosureV0> {
213 if !cascade_driver_census_payload_is_consistent_v0(census) {
214 return None;
215 }
216 derive_cascade_axis_reach_v0(census)
217}
218
219fn cascade_driver_census_payload_is_consistent_v0(census: &CascadeDriverCensusV0) -> bool {
220 let catalog = cascade_level_catalog_v0();
221 let levels = census
222 .levels
223 .iter()
224 .filter_map(|entry| cascade_level_from_name_v0(entry.level.as_str()))
225 .collect::<Vec<_>>();
226 let driven = census
227 .levels
228 .iter()
229 .filter(|entry| entry.status == "driven")
230 .filter_map(|entry| cascade_level_from_name_v0(entry.level.as_str()))
231 .collect::<Vec<_>>();
232 let inline_important_driver_count = census
233 .levels
234 .iter()
235 .flat_map(|entry| entry.driver_inputs.iter())
236 .filter(|input| input.as_str() == "inlineStyleImportant")
237 .count();
238 let expected_driven = cascade_origin_driver_catalog_v0()
239 .into_iter()
240 .map(|driver| driver.level)
241 .collect::<BTreeSet<_>>();
242 let all_levels_have_evidence = census.levels.iter().all(|entry| {
243 (entry.status == "driven" && !entry.driver_inputs.is_empty() && entry.follow_up.is_none())
244 || (entry.status == "deferred"
245 && entry.driver_inputs.is_empty()
246 && entry
247 .follow_up
248 .as_deref()
249 .is_some_and(|value| !value.is_empty()))
250 });
251 let expected_axes = cascade_winner_axis_catalog_from_authority_v0();
252 let producer_ids = census
253 .cascade_key_producers
254 .iter()
255 .map(|producer| {
256 (
257 producer.path.as_str(),
258 producer.symbol.as_str(),
259 producer.occurrence,
260 )
261 })
262 .collect::<BTreeSet<_>>();
263 let all_producers_have_consistent_evidence = census
264 .cascade_key_producers
265 .iter()
266 .all(cascade_key_producer_has_consistent_evidence_v0);
267 let all_axes_have_consistent_evidence =
268 census.winner_axes.iter().all(|entry| match entry.axis {
269 CascadeWinnerAxisV0::ScopeProximity => {
270 entry.status == CascadeDriverAxisStatusV0::AutomaticProductDriver
271 && entry.named_driver
272 == Some(CascadeAxisNamedDriverV0::LegacySelectorContextFallback)
273 }
274 _ => entry.status == CascadeDriverAxisStatusV0::Driven && entry.named_driver.is_none(),
275 });
276 let derived_reach = derive_cascade_axis_reach_v0(census);
277 census.schema_version == "0"
278 && census.product == "omena-cascade.driver-census"
279 && levels == catalog
280 && driven.into_iter().collect::<BTreeSet<_>>() == expected_driven
281 && inline_important_driver_count == 1
282 && all_levels_have_evidence
283 && census.winner_axes.len() == expected_axes.len()
284 && all_axes_have_consistent_evidence
285 && census
286 .winner_axes
287 .iter()
288 .map(|entry| entry.axis)
289 .eq(expected_axes)
290 && producer_ids.len() == census.cascade_key_producers.len()
291 && all_producers_have_consistent_evidence
292 && derived_reach.as_ref() == Some(&census.spec_axis_reach)
293}
294
295fn cascade_key_producer_has_consistent_evidence_v0(producer: &CascadeKeyProducerV0) -> bool {
296 if producer.path.is_empty() || producer.symbol.is_empty() || producer.occurrence == 0 {
297 return false;
298 }
299 matches!(
300 (producer.disposition, producer.scope_proximity_source),
301 (
302 CascadeKeyProducerDispositionV0::AutomaticProductDerived,
303 CascadeScopeProximitySourceV0::ConstantZero
304 | CascadeScopeProximitySourceV0::LegacySelectorContextFallback,
305 ) | (
306 CascadeKeyProducerDispositionV0::CallerSuppliedBoundary
307 | CascadeKeyProducerDispositionV0::Conformance,
308 CascadeScopeProximitySourceV0::CallerSupplied,
309 ) | (
310 CascadeKeyProducerDispositionV0::Generated,
311 CascadeScopeProximitySourceV0::GeneratedValue,
312 ) | (
313 CascadeKeyProducerDispositionV0::Fixture,
314 CascadeScopeProximitySourceV0::ConstantZero,
315 )
316 )
317}
318
319fn derive_cascade_axis_reach_v0(
320 census: &CascadeDriverCensusV0,
321) -> Option<CascadeAxisReachDisclosureV0> {
322 let driven_levels = census
323 .levels
324 .iter()
325 .filter(|entry| entry.status == "driven")
326 .filter_map(|entry| cascade_level_from_name_v0(entry.level.as_str()))
327 .collect::<BTreeSet<_>>();
328 let expected_driven_levels = cascade_origin_driver_catalog_v0()
329 .into_iter()
330 .map(|driver| driver.level)
331 .collect::<BTreeSet<_>>();
332 if driven_levels != expected_driven_levels
333 || !driven_levels.contains(&CascadeLevel::InlineNormal)
334 || !driven_levels.contains(&CascadeLevel::InlineImportant)
335 {
336 return None;
337 }
338
339 let axis_is_driven = |axis| {
340 census
341 .winner_axes
342 .iter()
343 .any(|entry| entry.axis == axis && entry.status.is_driven())
344 };
345 if !axis_is_driven(CascadeWinnerAxisV0::CascadeLevel)
346 || !axis_is_driven(CascadeWinnerAxisV0::LayerRank)
347 || !axis_is_driven(CascadeWinnerAxisV0::Specificity)
348 || !axis_is_driven(CascadeWinnerAxisV0::ScopeProximity)
349 || !axis_is_driven(CascadeWinnerAxisV0::SourceOrder)
350 {
351 return None;
352 }
353
354 let automatic_sources = census
355 .cascade_key_producers
356 .iter()
357 .filter(|producer| {
358 producer.disposition == CascadeKeyProducerDispositionV0::AutomaticProductDerived
359 })
360 .map(|producer| producer.scope_proximity_source)
361 .collect::<Vec<_>>();
362 if automatic_sources.is_empty()
363 || automatic_sources.iter().any(|source| {
364 !matches!(
365 source,
366 CascadeScopeProximitySourceV0::ConstantZero
367 | CascadeScopeProximitySourceV0::LegacySelectorContextFallback
368 )
369 })
370 || !automatic_sources
371 .contains(&CascadeScopeProximitySourceV0::LegacySelectorContextFallback)
372 {
373 return None;
374 }
375
376 Some(CascadeAxisReachDisclosureV0 {
380 origin_and_importance: CascadeAxisReachStatusV0::Modeled,
381 encapsulation_context: CascadeAxisReachStatusV0::OutOfFragment {
382 reason: CascadeAxisOutOfFragmentReasonV0::ShadowTreeEncapsulationContextUnmodeled,
383 },
384 style_attribute: CascadeAxisReachStatusV0::Modeled,
385 layers: CascadeAxisReachStatusV0::Modeled,
386 specificity: CascadeAxisReachStatusV0::Modeled,
387 scope_proximity: CascadeAxisReachStatusV0::NotReachedByProduct {
388 named_driver: CascadeAxisNamedDriverV0::LegacySelectorContextFallback,
389 },
390 order_of_appearance: CascadeAxisReachStatusV0::Modeled,
391 })
392}
393
394fn cascade_winner_axis_catalog_from_authority_v0() -> Vec<CascadeWinnerAxisV0> {
395 let mut axes = Vec::new();
396 for axis in cascade_key_axis_order_v0() {
397 let winner_axis = match axis {
398 CascadeKeyAxisV0::Level => CascadeWinnerAxisV0::CascadeLevel,
399 CascadeKeyAxisV0::LayerRank => CascadeWinnerAxisV0::LayerRank,
400 CascadeKeyAxisV0::ScopeProximity => CascadeWinnerAxisV0::ScopeProximity,
401 CascadeKeyAxisV0::SpecificityIds
402 | CascadeKeyAxisV0::SpecificityClasses
403 | CascadeKeyAxisV0::SpecificityElements => CascadeWinnerAxisV0::Specificity,
404 CascadeKeyAxisV0::SourceOrder => CascadeWinnerAxisV0::SourceOrder,
405 };
406 if axes.last() != Some(&winner_axis) {
407 axes.push(winner_axis);
408 }
409 }
410 axes
411}
412
413fn cascade_level_from_name_v0(name: &str) -> Option<CascadeLevel> {
414 cascade_level_catalog_v0()
415 .into_iter()
416 .find(|level| cascade_level_name_v0(*level) == name)
417}
418
419pub const fn cascade_level_for_origin(origin: CascadeOriginV0, important: bool) -> CascadeLevel {
420 match (origin, important) {
421 (CascadeOriginV0::UserAgent, false) => CascadeLevel::UserAgentNormal,
422 (CascadeOriginV0::User, false) => CascadeLevel::UserNormal,
423 (CascadeOriginV0::Author, false) => CascadeLevel::AuthorNormal,
424 (CascadeOriginV0::Inline, false) => CascadeLevel::InlineNormal,
425 (CascadeOriginV0::UserAgent, true) => CascadeLevel::UserAgentImportant,
426 (CascadeOriginV0::User, true) => CascadeLevel::UserImportant,
427 (CascadeOriginV0::Author, true) => CascadeLevel::AuthorImportant,
428 (CascadeOriginV0::Inline, true) => CascadeLevel::InlineImportant,
429 }
430}
431
432pub const fn cascade_level_catalog_v0() -> [CascadeLevel; 10] {
433 [
434 CascadeLevel::UserAgentNormal,
435 CascadeLevel::UserNormal,
436 CascadeLevel::AuthorNormal,
437 CascadeLevel::InlineNormal,
438 CascadeLevel::Animation,
439 CascadeLevel::AuthorImportant,
440 CascadeLevel::InlineImportant,
441 CascadeLevel::UserImportant,
442 CascadeLevel::UserAgentImportant,
443 CascadeLevel::Transition,
444 ]
445}
446
447pub const fn cascade_level_name_v0(level: CascadeLevel) -> &'static str {
448 match level {
449 CascadeLevel::UserAgentNormal => "userAgentNormal",
450 CascadeLevel::UserNormal => "userNormal",
451 CascadeLevel::AuthorNormal => "authorNormal",
452 CascadeLevel::InlineNormal => "inlineNormal",
453 CascadeLevel::Animation => "animation",
454 CascadeLevel::InlineImportant => "inlineImportant",
455 CascadeLevel::AuthorImportant => "authorImportant",
456 CascadeLevel::UserImportant => "userImportant",
457 CascadeLevel::UserAgentImportant => "userAgentImportant",
458 CascadeLevel::Transition => "transition",
459 }
460}
461
462pub const fn cascade_origin_driver_catalog_v0() -> [CascadeOriginDriverV0; 8] {
463 [
464 origin_driver(CascadeOriginV0::UserAgent, false),
465 origin_driver(CascadeOriginV0::User, false),
466 origin_driver(CascadeOriginV0::Author, false),
467 origin_driver(CascadeOriginV0::Inline, false),
468 origin_driver(CascadeOriginV0::Author, true),
469 origin_driver(CascadeOriginV0::Inline, true),
470 origin_driver(CascadeOriginV0::User, true),
471 origin_driver(CascadeOriginV0::UserAgent, true),
472 ]
473}
474
475const fn origin_driver(origin: CascadeOriginV0, important: bool) -> CascadeOriginDriverV0 {
476 CascadeOriginDriverV0 {
477 origin,
478 important,
479 level: cascade_level_for_origin(origin, important),
480 }
481}
482
483#[cfg(test)]
484mod tests {
485 use super::*;
486
487 #[test]
488 fn embedded_driver_census_derives_the_typed_axis_reach_disclosure() {
489 assert!(cascade_driver_census_is_consistent_v0());
490 assert_eq!(
491 summarize_cascade_axis_reach_v0(),
492 Some(CascadeAxisReachDisclosureV0 {
493 origin_and_importance: CascadeAxisReachStatusV0::Modeled,
494 encapsulation_context: CascadeAxisReachStatusV0::OutOfFragment {
495 reason:
496 CascadeAxisOutOfFragmentReasonV0::ShadowTreeEncapsulationContextUnmodeled,
497 },
498 style_attribute: CascadeAxisReachStatusV0::Modeled,
499 layers: CascadeAxisReachStatusV0::Modeled,
500 specificity: CascadeAxisReachStatusV0::Modeled,
501 scope_proximity: CascadeAxisReachStatusV0::NotReachedByProduct {
502 named_driver: CascadeAxisNamedDriverV0::LegacySelectorContextFallback,
503 },
504 order_of_appearance: CascadeAxisReachStatusV0::Modeled,
505 })
506 );
507 }
508
509 #[test]
510 fn caller_supplied_proximity_surfaces_are_excluded_from_the_automatic_product_driver()
511 -> Result<(), &'static str> {
512 let census = cascade_driver_census_v0().ok_or("embedded census must parse")?;
513 let caller_supplied = census
514 .cascade_key_producers
515 .iter()
516 .filter(|producer| {
517 producer.disposition == CascadeKeyProducerDispositionV0::CallerSuppliedBoundary
518 })
519 .collect::<Vec<_>>();
520 assert_eq!(caller_supplied.len(), 3);
521 assert!(caller_supplied.iter().all(|producer| {
522 producer.scope_proximity_source == CascadeScopeProximitySourceV0::CallerSupplied
523 }));
524 assert!(caller_supplied.iter().any(|producer| {
525 producer.path == "rust/crates/omena-bundler/src/lib.rs"
526 && producer.symbol == "LinkedStylesheetRuleV0::cascade_key_with_global_source_order"
527 }));
528 assert!(caller_supplied.iter().any(|producer| {
529 producer.path == "rust/crates/omena-cascade-proof/src/proof_kernel.rs"
530 && producer.symbol == "cascade_key_from_certificate_v0"
531 }));
532 assert!(caller_supplied.iter().any(|producer| {
533 producer.path == "rust/crates/omena-transform-passes/src/runtime/winner_equality.rs"
534 && producer.symbol == "winner_for_pair"
535 }));
536
537 let automatic = census
538 .cascade_key_producers
539 .iter()
540 .filter(|producer| {
541 producer.disposition == CascadeKeyProducerDispositionV0::AutomaticProductDerived
542 })
543 .collect::<Vec<_>>();
544 assert!(automatic.iter().all(|producer| {
545 matches!(
546 producer.scope_proximity_source,
547 CascadeScopeProximitySourceV0::ConstantZero
548 | CascadeScopeProximitySourceV0::LegacySelectorContextFallback
549 )
550 }));
551 assert!(automatic.iter().any(|producer| {
552 producer.path == "rust/crates/omena-semantic/src/design_tokens.rs"
553 && producer.scope_proximity_source
554 == CascadeScopeProximitySourceV0::LegacySelectorContextFallback
555 }));
556 Ok(())
557 }
558
559 #[test]
560 fn inconsistent_axis_reach_census_fails_closed() -> Result<(), &'static str> {
561 let mut census = cascade_driver_census_v0()
562 .ok_or("embedded census must parse")?
563 .clone();
564 census.spec_axis_reach.scope_proximity = CascadeAxisReachStatusV0::Modeled;
565
566 assert_eq!(summarize_cascade_axis_reach_from_census_v0(&census), None);
567 Ok(())
568 }
569}