Skip to main content

omena_cascade/
lib.rs

1//! Cascade-formal substrate for the Omena CSS track.
2//!
3//! The crate starts with the load-bearing algebra from the research plan:
4//! lexicographic cascade keys, specificity, provenance proofs, and a finite
5//! custom-property substitution function with explicit cycle handling.
6
7use serde::Serialize;
8use std::{
9    cmp::{Ordering, Reverse},
10    collections::{BTreeMap, BTreeSet},
11};
12
13#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Hash, Serialize)]
14#[serde(rename_all = "camelCase")]
15pub enum CascadeLevel {
16    UserAgentNormal,
17    UserNormal,
18    AuthorNormal,
19    InlineNormal,
20    Animation,
21    AuthorImportant,
22    UserImportant,
23    UserAgentImportant,
24    Transition,
25}
26
27#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Hash, Serialize)]
28#[serde(rename_all = "camelCase")]
29pub struct LayerRank(pub i32);
30
31#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Serialize)]
32#[serde(rename_all = "camelCase")]
33pub struct Specificity {
34    pub ids: u32,
35    pub classes: u32,
36    pub elements: u32,
37}
38
39impl Specificity {
40    pub const ZERO: Self = Self {
41        ids: 0,
42        classes: 0,
43        elements: 0,
44    };
45
46    pub const fn new(ids: u32, classes: u32, elements: u32) -> Self {
47        Self {
48            ids,
49            classes,
50            elements,
51        }
52    }
53}
54
55impl Ord for Specificity {
56    fn cmp(&self, other: &Self) -> Ordering {
57        (self.ids, self.classes, self.elements).cmp(&(other.ids, other.classes, other.elements))
58    }
59}
60
61impl PartialOrd for Specificity {
62    fn partial_cmp(&self, other: &Self) -> Option<Ordering> {
63        Some(self.cmp(other))
64    }
65}
66
67#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Serialize)]
68#[serde(rename_all = "camelCase")]
69pub struct CascadeKey {
70    pub level: CascadeLevel,
71    pub layer_rank: LayerRank,
72    pub scope_proximity: u32,
73    pub specificity: Specificity,
74    pub source_order: u32,
75}
76
77impl CascadeKey {
78    pub const fn new(
79        level: CascadeLevel,
80        layer_rank: LayerRank,
81        scope_proximity: u32,
82        specificity: Specificity,
83        source_order: u32,
84    ) -> Self {
85        Self {
86            level,
87            layer_rank,
88            scope_proximity,
89            specificity,
90            source_order,
91        }
92    }
93}
94
95impl Ord for CascadeKey {
96    fn cmp(&self, other: &Self) -> Ordering {
97        self.level
98            .cmp(&other.level)
99            .then_with(|| self.layer_rank.cmp(&other.layer_rank))
100            .then_with(|| other.scope_proximity.cmp(&self.scope_proximity))
101            .then_with(|| self.specificity.cmp(&other.specificity))
102            .then_with(|| self.source_order.cmp(&other.source_order))
103    }
104}
105
106impl PartialOrd for CascadeKey {
107    fn partial_cmp(&self, other: &Self) -> Option<Ordering> {
108        Some(self.cmp(other))
109    }
110}
111
112#[derive(Debug, Clone, PartialEq, Eq, Serialize)]
113#[serde(rename_all = "camelCase")]
114pub struct CascadeDeclaration {
115    pub id: String,
116    pub property: String,
117    pub value: CascadeValue,
118    pub key: CascadeKey,
119}
120
121#[derive(Debug, Clone, PartialEq, Eq, Serialize)]
122#[serde(rename_all = "camelCase")]
123pub struct CascadeProof {
124    pub declaration_id: String,
125    pub property: String,
126    pub level: CascadeLevel,
127    pub layer_rank: LayerRank,
128    pub scope_proximity: u32,
129    pub specificity: Specificity,
130    pub source_order: u32,
131}
132
133impl CascadeProof {
134    pub fn from_declaration(declaration: &CascadeDeclaration) -> Self {
135        Self {
136            declaration_id: declaration.id.clone(),
137            property: declaration.property.clone(),
138            level: declaration.key.level,
139            layer_rank: declaration.key.layer_rank,
140            scope_proximity: declaration.key.scope_proximity,
141            specificity: declaration.key.specificity,
142            source_order: declaration.key.source_order,
143        }
144    }
145}
146
147#[derive(Debug, Clone, PartialEq, Eq, Serialize)]
148#[serde(rename_all = "camelCase")]
149pub enum CascadeOutcome {
150    Definite {
151        winner: CascadeDeclaration,
152        proof: CascadeProof,
153        also_considered: Vec<CascadeDeclaration>,
154    },
155    RankedSet(Vec<CascadeDeclaration>),
156    Inherit,
157    Top,
158}
159
160#[derive(Debug, Clone, PartialEq, Eq, Serialize)]
161#[serde(rename_all = "camelCase")]
162pub enum CascadeValue {
163    Literal(String),
164    Var {
165        name: String,
166        fallback: Option<Box<CascadeValue>>,
167    },
168    GuaranteedInvalid,
169    Unset,
170}
171
172#[derive(Debug, Clone, PartialEq, Eq, Serialize)]
173#[serde(rename_all = "camelCase")]
174pub enum SelectorContextMatchKind {
175    NoMatch,
176    Global,
177    Root,
178    Exact,
179    ContainsSelector,
180}
181
182#[derive(Debug, Clone, PartialEq, Eq, Serialize)]
183#[serde(rename_all = "camelCase")]
184pub struct SelectorContextWitness {
185    pub kind: SelectorContextMatchKind,
186    pub matched: bool,
187    pub rank: usize,
188    pub declaration_selector: Option<String>,
189    pub reference_selector: Option<String>,
190}
191
192impl SelectorContextWitness {
193    pub fn no_match() -> Self {
194        Self {
195            kind: SelectorContextMatchKind::NoMatch,
196            matched: false,
197            rank: 0,
198            declaration_selector: None,
199            reference_selector: None,
200        }
201    }
202}
203
204#[derive(Debug, Clone, PartialEq, Eq, Serialize)]
205#[serde(rename_all = "camelCase")]
206pub struct ElementSignature {
207    pub tag: Option<String>,
208    pub id: Option<String>,
209    pub classes: BTreeSet<String>,
210    pub attributes: BTreeSet<String>,
211    pub pseudo_states: BTreeSet<String>,
212    pub classes_are_exact: bool,
213    pub attributes_are_exact: bool,
214    pub pseudo_states_are_exact: bool,
215    pub tag_is_exact: bool,
216    pub id_is_exact: bool,
217}
218
219impl ElementSignature {
220    pub fn concrete(
221        tag: Option<impl Into<String>>,
222        id: Option<impl Into<String>>,
223        classes: impl IntoIterator<Item = impl Into<String>>,
224    ) -> Self {
225        Self {
226            tag: tag.map(Into::into),
227            id: id.map(Into::into),
228            classes: classes.into_iter().map(Into::into).collect(),
229            attributes: BTreeSet::new(),
230            pseudo_states: BTreeSet::new(),
231            classes_are_exact: true,
232            attributes_are_exact: true,
233            pseudo_states_are_exact: true,
234            tag_is_exact: true,
235            id_is_exact: true,
236        }
237    }
238
239    pub fn at_least_classes(classes: impl IntoIterator<Item = impl Into<String>>) -> Self {
240        Self {
241            classes_are_exact: false,
242            ..Self::concrete(None::<String>, None::<String>, classes)
243        }
244    }
245}
246
247#[derive(Debug, Clone, PartialEq, Eq, Serialize)]
248#[serde(rename_all = "camelCase")]
249pub struct SelectorSignature {
250    pub selector: String,
251    pub required_tag: Option<String>,
252    pub required_id: Option<String>,
253    pub required_classes: BTreeSet<String>,
254    pub required_attributes: BTreeSet<String>,
255    pub required_pseudo_states: BTreeSet<String>,
256    pub specificity: Specificity,
257}
258
259#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Hash, Serialize)]
260#[serde(rename_all = "camelCase")]
261pub enum SelectorMatchVerdict {
262    No,
263    Maybe,
264    Yes,
265}
266
267#[derive(Debug, Clone, PartialEq, Eq, Serialize)]
268#[serde(rename_all = "camelCase")]
269pub enum SelectorMatchReason {
270    Universal,
271    SimpleCompound,
272    SelectorList,
273    MissingTag,
274    MissingId,
275    MissingClass,
276    MissingAttribute,
277    MissingPseudoState,
278    UnsupportedSelector,
279}
280
281#[derive(Debug, Clone, PartialEq, Eq, Serialize)]
282#[serde(rename_all = "camelCase")]
283pub struct SelectorMatchWitness {
284    pub selector: String,
285    pub matched_branch: Option<String>,
286    pub verdict: SelectorMatchVerdict,
287    pub reason: SelectorMatchReason,
288    pub specificity: Specificity,
289    pub missing_tag: Option<String>,
290    pub missing_id: Option<String>,
291    pub missing_classes: BTreeSet<String>,
292    pub missing_attributes: BTreeSet<String>,
293    pub missing_pseudo_states: BTreeSet<String>,
294    pub unsupported_branches: Vec<String>,
295}
296
297impl SelectorMatchWitness {
298    fn unsupported(selector: &str) -> Self {
299        Self {
300            selector: selector.to_string(),
301            matched_branch: Some(selector.to_string()),
302            verdict: SelectorMatchVerdict::Maybe,
303            reason: SelectorMatchReason::UnsupportedSelector,
304            specificity: Specificity::ZERO,
305            missing_tag: None,
306            missing_id: None,
307            missing_classes: BTreeSet::new(),
308            missing_attributes: BTreeSet::new(),
309            missing_pseudo_states: BTreeSet::new(),
310            unsupported_branches: vec![selector.to_string()],
311        }
312    }
313}
314
315#[derive(Debug, Clone, PartialEq, Eq, Serialize)]
316#[serde(rename_all = "camelCase")]
317pub struct CascadeBoundarySummary {
318    pub product: &'static str,
319    pub ordering_model: &'static str,
320    pub substitution_model: &'static str,
321    pub ready_surfaces: Vec<&'static str>,
322    pub not_ready_surfaces: Vec<&'static str>,
323}
324
325#[derive(Debug, Clone, PartialEq, Eq, Serialize)]
326#[serde(rename_all = "camelCase")]
327pub struct CascadeConformanceSeedCase {
328    pub name: String,
329    pub property: &'static str,
330    pub declarations: Vec<CascadeDeclaration>,
331    pub expected_outcome: &'static str,
332    pub expected_winner_id: Option<String>,
333}
334
335#[derive(Debug, Clone, PartialEq, Eq, Serialize)]
336#[serde(rename_all = "camelCase")]
337pub struct CascadeConformanceSeedResult {
338    pub name: String,
339    pub passed: bool,
340    pub expected_outcome: &'static str,
341    pub actual_outcome: &'static str,
342    pub expected_winner_id: Option<String>,
343    pub actual_winner_id: Option<String>,
344}
345
346#[derive(Debug, Clone, PartialEq, Eq, Serialize)]
347#[serde(rename_all = "camelCase")]
348pub struct CascadeConformanceSeedReport {
349    pub schema_version: &'static str,
350    pub product: &'static str,
351    pub case_count: usize,
352    pub passed_count: usize,
353    pub failed_count: usize,
354    pub results: Vec<CascadeConformanceSeedResult>,
355}
356
357#[derive(Debug, Clone, PartialEq, Eq, Serialize)]
358#[serde(rename_all = "camelCase")]
359pub struct CascadeEvaluationFuzzCaseV0 {
360    pub seed: u64,
361    pub declaration_count: usize,
362}
363
364#[derive(Debug, Clone, PartialEq, Eq, Serialize)]
365#[serde(rename_all = "camelCase")]
366pub struct CascadeEvaluationFuzzResultV0 {
367    pub seed: u64,
368    pub declaration_count: usize,
369    pub actual_winner_id: Option<String>,
370    pub expected_winner_id: Option<String>,
371    pub ranked_count: usize,
372    pub passed: bool,
373}
374
375#[derive(Debug, Clone, PartialEq, Eq, Serialize)]
376#[serde(rename_all = "camelCase")]
377pub struct VarSubstitutionFuzzCaseV0 {
378    pub seed: u64,
379    pub chain_len: usize,
380    pub cycle: bool,
381}
382
383#[derive(Debug, Clone, PartialEq, Eq, Serialize)]
384#[serde(rename_all = "camelCase")]
385pub struct VarSubstitutionFuzzResultV0 {
386    pub seed: u64,
387    pub chain_len: usize,
388    pub cycle: bool,
389    pub result: CascadeValue,
390    pub expected: CascadeValue,
391    pub passed: bool,
392}
393
394#[derive(Debug, Clone, PartialEq, Eq, Serialize)]
395#[serde(rename_all = "camelCase")]
396pub struct CascadeFuzzSeedReportV0 {
397    pub schema_version: &'static str,
398    pub product: &'static str,
399    pub case_count: usize,
400    pub passed_count: usize,
401    pub failed_count: usize,
402    pub cascade_results: Vec<CascadeEvaluationFuzzResultV0>,
403    pub var_results: Vec<VarSubstitutionFuzzResultV0>,
404}
405
406#[derive(Debug, Clone, PartialEq, Eq, Serialize)]
407#[serde(rename_all = "camelCase")]
408pub struct BoxLonghandInputV0 {
409    pub property: String,
410    pub value: String,
411    pub important: bool,
412    pub source_order: u32,
413}
414
415#[derive(Debug, Clone, PartialEq, Eq, Serialize)]
416#[serde(rename_all = "camelCase")]
417pub struct ShorthandCombinationProofV0 {
418    pub schema_version: &'static str,
419    pub product: &'static str,
420    pub shorthand_property: String,
421    pub accepted: bool,
422    pub blocked_reason: Option<&'static str>,
423    pub ordered_longhand_properties: Vec<String>,
424    pub provenance_preserved: bool,
425    pub cascade_safe_witness: String,
426}
427
428#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize)]
429#[serde(rename_all = "camelCase")]
430pub enum StaticSupportsAssumptionV0 {
431    ModernBrowser,
432}
433
434#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize)]
435#[serde(rename_all = "camelCase")]
436pub enum StaticSupportsEvalVerdictV0 {
437    AlwaysTrue,
438    AlwaysFalse,
439    Unknown,
440}
441
442#[derive(Debug, Clone, PartialEq, Eq, Serialize)]
443#[serde(rename_all = "camelCase")]
444pub struct StaticSupportsEvalWitnessV0 {
445    pub schema_version: &'static str,
446    pub product: &'static str,
447    pub condition: String,
448    pub assumption: StaticSupportsAssumptionV0,
449    pub verdict: StaticSupportsEvalVerdictV0,
450    pub reason: &'static str,
451    pub provenance_preserved: bool,
452}
453
454#[derive(Debug, Clone, PartialEq, Eq, Serialize)]
455#[serde(rename_all = "camelCase")]
456pub struct ScopeFlattenInputV0 {
457    pub root_selector: String,
458    pub limit_selector: Option<String>,
459    pub scoped_rule_count: usize,
460    pub peer_scope_count: usize,
461    pub competing_unscoped_rule_count: usize,
462    pub inside_layer: bool,
463}
464
465#[derive(Debug, Clone, PartialEq, Eq, Serialize)]
466#[serde(rename_all = "camelCase")]
467pub struct ScopeFlattenProofV0 {
468    pub schema_version: &'static str,
469    pub product: &'static str,
470    pub accepted: bool,
471    pub blocked_reason: Option<&'static str>,
472    pub root_selector: String,
473    pub provenance_preserved: bool,
474    pub cascade_safe_witness: String,
475}
476
477#[derive(Debug, Clone, PartialEq, Eq, Serialize)]
478#[serde(rename_all = "camelCase")]
479pub struct LayerFlattenInputV0 {
480    pub layer_name: Option<String>,
481    pub layer_rule_count: usize,
482    pub peer_layer_count: usize,
483    pub unlayered_rule_count: usize,
484    pub important_declaration_count: usize,
485    pub closed_bundle: bool,
486}
487
488#[derive(Debug, Clone, PartialEq, Eq, Serialize)]
489#[serde(rename_all = "camelCase")]
490pub struct LayerFlattenProofV0 {
491    pub schema_version: &'static str,
492    pub product: &'static str,
493    pub accepted: bool,
494    pub blocked_reason: Option<&'static str>,
495    pub layer_name: Option<String>,
496    pub provenance_preserved: bool,
497    pub cascade_safe_witness: String,
498}
499
500pub type CustomPropertyEnv = BTreeMap<String, CascadeValue>;
501
502pub fn summarize_cascade_boundary() -> CascadeBoundarySummary {
503    CascadeBoundarySummary {
504        product: "omena-cascade.boundary",
505        ordering_model: "lexicographicCascadeKey",
506        substitution_model: "finiteCustomPropertyLeastFixedPoint",
507        ready_surfaces: vec![
508            "cascadeKeyOrdering",
509            "specificityOrdering",
510            "cascadeOutcomeProof",
511            "genericCascadeWinner",
512            "semanticDesignTokenRanking",
513            "queryReadCascadeAtPosition",
514            "selectorContextWitness",
515            "selectorMatchWitness",
516            "cascadeConformanceSeedCorpus",
517            "customPropertySubstitution",
518            "cycleToGuaranteedInvalid",
519            "shorthandCombinationProof",
520            "supportsStaticEvalWitness",
521            "scopeFlattenProof",
522            "layerFlattenProof",
523            "wptCascadeSeedCorpus",
524        ],
525        not_ready_surfaces: vec!["fullWptCascadeCorpus"],
526    }
527}
528
529pub fn run_cascade_conformance_seed_corpus() -> CascadeConformanceSeedReport {
530    let results = cascade_conformance_seed_cases()
531        .into_iter()
532        .map(run_cascade_conformance_seed_case)
533        .collect::<Vec<_>>();
534    let passed_count = results.iter().filter(|result| result.passed).count();
535    let case_count = results.len();
536
537    CascadeConformanceSeedReport {
538        schema_version: "0",
539        product: "omena-cascade.conformance-seed-corpus",
540        case_count,
541        passed_count,
542        failed_count: case_count.saturating_sub(passed_count),
543        results,
544    }
545}
546
547pub fn run_wpt_cascade_seed_corpus() -> CascadeConformanceSeedReport {
548    let results = wpt_cascade_seed_cases()
549        .into_iter()
550        .map(run_cascade_conformance_seed_case)
551        .collect::<Vec<_>>();
552    let passed_count = results.iter().filter(|result| result.passed).count();
553    let case_count = results.len();
554
555    CascadeConformanceSeedReport {
556        schema_version: "0",
557        product: "omena-cascade.wpt-cascade-seed-corpus",
558        case_count,
559        passed_count,
560        failed_count: case_count.saturating_sub(passed_count),
561        results,
562    }
563}
564
565fn run_cascade_conformance_seed_case(
566    case: CascadeConformanceSeedCase,
567) -> CascadeConformanceSeedResult {
568    let outcome = cascade_property(case.declarations, case.property);
569    let (actual_outcome, actual_winner_id) = match outcome {
570        CascadeOutcome::Definite { winner, .. } => ("definite", Some(winner.id)),
571        CascadeOutcome::RankedSet(_) => ("rankedSet", None),
572        CascadeOutcome::Inherit => ("inherit", None),
573        CascadeOutcome::Top => ("top", None),
574    };
575    let passed =
576        actual_outcome == case.expected_outcome && actual_winner_id == case.expected_winner_id;
577
578    CascadeConformanceSeedResult {
579        name: case.name,
580        passed,
581        expected_outcome: case.expected_outcome,
582        actual_outcome,
583        expected_winner_id: case.expected_winner_id,
584        actual_winner_id,
585    }
586}
587
588fn cascade_conformance_seed_cases() -> Vec<CascadeConformanceSeedCase> {
589    vec![
590        CascadeConformanceSeedCase {
591            name: "source-order-breaks-identical-key".to_string(),
592            property: "color",
593            declarations: vec![
594                conformance_decl(
595                    "source-earlier",
596                    "color",
597                    "red",
598                    conformance_key(
599                        CascadeLevel::AuthorNormal,
600                        0,
601                        0,
602                        Specificity::new(0, 1, 0),
603                        1,
604                    ),
605                ),
606                conformance_decl(
607                    "source-later",
608                    "color",
609                    "blue",
610                    conformance_key(
611                        CascadeLevel::AuthorNormal,
612                        0,
613                        0,
614                        Specificity::new(0, 1, 0),
615                        2,
616                    ),
617                ),
618            ],
619            expected_outcome: "definite",
620            expected_winner_id: Some("source-later".to_string()),
621        },
622        CascadeConformanceSeedCase {
623            name: "specificity-beats-source-order".to_string(),
624            property: "color",
625            declarations: vec![
626                conformance_decl(
627                    "specificity-low-later",
628                    "color",
629                    "red",
630                    conformance_key(
631                        CascadeLevel::AuthorNormal,
632                        0,
633                        0,
634                        Specificity::new(0, 1, 0),
635                        2,
636                    ),
637                ),
638                conformance_decl(
639                    "specificity-high-earlier",
640                    "color",
641                    "blue",
642                    conformance_key(
643                        CascadeLevel::AuthorNormal,
644                        0,
645                        0,
646                        Specificity::new(1, 0, 0),
647                        1,
648                    ),
649                ),
650            ],
651            expected_outcome: "definite",
652            expected_winner_id: Some("specificity-high-earlier".to_string()),
653        },
654        CascadeConformanceSeedCase {
655            name: "important-origin-beats-inline-normal".to_string(),
656            property: "color",
657            declarations: vec![
658                conformance_decl(
659                    "inline-normal",
660                    "color",
661                    "red",
662                    conformance_key(
663                        CascadeLevel::InlineNormal,
664                        0,
665                        0,
666                        Specificity::new(1, 0, 0),
667                        2,
668                    ),
669                ),
670                conformance_decl(
671                    "author-important",
672                    "color",
673                    "blue",
674                    conformance_key(
675                        CascadeLevel::AuthorImportant,
676                        0,
677                        0,
678                        Specificity::new(0, 1, 0),
679                        1,
680                    ),
681                ),
682            ],
683            expected_outcome: "definite",
684            expected_winner_id: Some("author-important".to_string()),
685        },
686        CascadeConformanceSeedCase {
687            name: "layer-rank-beats-specificity-within-level".to_string(),
688            property: "color",
689            declarations: vec![
690                conformance_decl(
691                    "lower-layer-specific",
692                    "color",
693                    "red",
694                    conformance_key(
695                        CascadeLevel::AuthorNormal,
696                        1,
697                        0,
698                        Specificity::new(1, 0, 0),
699                        2,
700                    ),
701                ),
702                conformance_decl(
703                    "higher-layer",
704                    "color",
705                    "blue",
706                    conformance_key(
707                        CascadeLevel::AuthorNormal,
708                        2,
709                        0,
710                        Specificity::new(0, 1, 0),
711                        1,
712                    ),
713                ),
714            ],
715            expected_outcome: "definite",
716            expected_winner_id: Some("higher-layer".to_string()),
717        },
718        CascadeConformanceSeedCase {
719            name: "scope-proximity-beats-specificity-tie".to_string(),
720            property: "color",
721            declarations: vec![
722                conformance_decl(
723                    "far-scope",
724                    "color",
725                    "red",
726                    conformance_key(
727                        CascadeLevel::AuthorNormal,
728                        0,
729                        5,
730                        Specificity::new(0, 1, 0),
731                        2,
732                    ),
733                ),
734                conformance_decl(
735                    "near-scope",
736                    "color",
737                    "blue",
738                    conformance_key(
739                        CascadeLevel::AuthorNormal,
740                        0,
741                        1,
742                        Specificity::new(0, 1, 0),
743                        1,
744                    ),
745                ),
746            ],
747            expected_outcome: "definite",
748            expected_winner_id: Some("near-scope".to_string()),
749        },
750        CascadeConformanceSeedCase {
751            name: "missing-property-inherits".to_string(),
752            property: "background",
753            declarations: vec![conformance_decl(
754                "color-only",
755                "color",
756                "red",
757                conformance_key(
758                    CascadeLevel::AuthorNormal,
759                    0,
760                    0,
761                    Specificity::new(0, 1, 0),
762                    1,
763                ),
764            )],
765            expected_outcome: "inherit",
766            expected_winner_id: None,
767        },
768    ]
769}
770
771fn wpt_cascade_seed_cases() -> Vec<CascadeConformanceSeedCase> {
772    let levels = [
773        CascadeLevel::UserAgentNormal,
774        CascadeLevel::UserNormal,
775        CascadeLevel::AuthorNormal,
776        CascadeLevel::InlineNormal,
777        CascadeLevel::Animation,
778        CascadeLevel::AuthorImportant,
779        CascadeLevel::UserImportant,
780        CascadeLevel::UserAgentImportant,
781        CascadeLevel::Transition,
782    ];
783    let specificities = [
784        Specificity::new(0, 0, 1),
785        Specificity::new(0, 1, 0),
786        Specificity::new(1, 0, 0),
787    ];
788
789    let mut cases = Vec::new();
790
791    for left in levels {
792        for right in levels {
793            if left == right {
794                continue;
795            }
796
797            let winner = if left > right { "left" } else { "right" };
798            cases.push(CascadeConformanceSeedCase {
799                name: format!("wpt-origin-importance-order-{left:?}-vs-{right:?}"),
800                property: "color",
801                declarations: vec![
802                    conformance_decl(
803                        "left",
804                        "color",
805                        "red",
806                        conformance_key(left, 0, 0, Specificity::new(0, 1, 0), 1),
807                    ),
808                    conformance_decl(
809                        "right",
810                        "color",
811                        "blue",
812                        conformance_key(right, 0, 0, Specificity::new(0, 1, 0), 2),
813                    ),
814                ],
815                expected_outcome: "definite",
816                expected_winner_id: Some(winner.to_string()),
817            });
818        }
819    }
820
821    for layer_left in -3..=3 {
822        for layer_right in -3..=3 {
823            if layer_left == layer_right {
824                continue;
825            }
826
827            let winner = if layer_left > layer_right {
828                "left"
829            } else {
830                "right"
831            };
832            cases.push(CascadeConformanceSeedCase {
833                name: format!("wpt-layer-order-{layer_left}-vs-{layer_right}"),
834                property: "color",
835                declarations: vec![
836                    conformance_decl(
837                        "left",
838                        "color",
839                        "red",
840                        conformance_key(
841                            CascadeLevel::AuthorNormal,
842                            layer_left,
843                            0,
844                            Specificity::new(0, 1, 0),
845                            2,
846                        ),
847                    ),
848                    conformance_decl(
849                        "right",
850                        "color",
851                        "blue",
852                        conformance_key(
853                            CascadeLevel::AuthorNormal,
854                            layer_right,
855                            0,
856                            Specificity::new(1, 0, 0),
857                            1,
858                        ),
859                    ),
860                ],
861                expected_outcome: "definite",
862                expected_winner_id: Some(winner.to_string()),
863            });
864        }
865    }
866
867    for scope_left in 0..=7 {
868        for scope_right in 0..=7 {
869            if scope_left == scope_right {
870                continue;
871            }
872
873            let winner = if scope_left < scope_right {
874                "left"
875            } else {
876                "right"
877            };
878            cases.push(CascadeConformanceSeedCase {
879                name: format!("wpt-scope-proximity-{scope_left}-vs-{scope_right}"),
880                property: "color",
881                declarations: vec![
882                    conformance_decl(
883                        "left",
884                        "color",
885                        "red",
886                        conformance_key(
887                            CascadeLevel::AuthorNormal,
888                            0,
889                            scope_left,
890                            Specificity::new(0, 1, 0),
891                            2,
892                        ),
893                    ),
894                    conformance_decl(
895                        "right",
896                        "color",
897                        "blue",
898                        conformance_key(
899                            CascadeLevel::AuthorNormal,
900                            0,
901                            scope_right,
902                            Specificity::new(0, 1, 0),
903                            1,
904                        ),
905                    ),
906                ],
907                expected_outcome: "definite",
908                expected_winner_id: Some(winner.to_string()),
909            });
910        }
911    }
912
913    for left in specificities {
914        for right in specificities {
915            if left == right {
916                continue;
917            }
918
919            let winner = if left > right { "left" } else { "right" };
920            cases.push(CascadeConformanceSeedCase {
921                name: format!("wpt-specificity-order-{left:?}-vs-{right:?}"),
922                property: "color",
923                declarations: vec![
924                    conformance_decl(
925                        "left",
926                        "color",
927                        "red",
928                        conformance_key(CascadeLevel::AuthorNormal, 0, 0, left, 1),
929                    ),
930                    conformance_decl(
931                        "right",
932                        "color",
933                        "blue",
934                        conformance_key(CascadeLevel::AuthorNormal, 0, 0, right, 2),
935                    ),
936                ],
937                expected_outcome: "definite",
938                expected_winner_id: Some(winner.to_string()),
939            });
940        }
941    }
942
943    for source_left in 0..=15 {
944        for source_right in 0..=15 {
945            if source_left == source_right {
946                continue;
947            }
948
949            let winner = if source_left > source_right {
950                "left"
951            } else {
952                "right"
953            };
954            cases.push(CascadeConformanceSeedCase {
955                name: format!("wpt-source-order-{source_left}-vs-{source_right}"),
956                property: "color",
957                declarations: vec![
958                    conformance_decl(
959                        "left",
960                        "color",
961                        "red",
962                        conformance_key(
963                            CascadeLevel::AuthorNormal,
964                            0,
965                            0,
966                            Specificity::new(0, 1, 0),
967                            source_left,
968                        ),
969                    ),
970                    conformance_decl(
971                        "right",
972                        "color",
973                        "blue",
974                        conformance_key(
975                            CascadeLevel::AuthorNormal,
976                            0,
977                            0,
978                            Specificity::new(0, 1, 0),
979                            source_right,
980                        ),
981                    ),
982                ],
983                expected_outcome: "definite",
984                expected_winner_id: Some(winner.to_string()),
985            });
986        }
987    }
988
989    cases
990}
991
992fn conformance_key(
993    level: CascadeLevel,
994    layer_rank: i32,
995    scope_proximity: u32,
996    specificity: Specificity,
997    source_order: u32,
998) -> CascadeKey {
999    CascadeKey::new(
1000        level,
1001        LayerRank(layer_rank),
1002        scope_proximity,
1003        specificity,
1004        source_order,
1005    )
1006}
1007
1008fn conformance_decl(id: &str, property: &str, value: &str, key: CascadeKey) -> CascadeDeclaration {
1009    CascadeDeclaration {
1010        id: id.to_string(),
1011        property: property.to_string(),
1012        value: CascadeValue::Literal(value.to_string()),
1013        key,
1014    }
1015}
1016
1017pub fn cascade_property(
1018    declarations: impl IntoIterator<Item = CascadeDeclaration>,
1019    property: &str,
1020) -> CascadeOutcome {
1021    let mut matching: Vec<CascadeDeclaration> = declarations
1022        .into_iter()
1023        .filter(|declaration| declaration.property == property)
1024        .collect();
1025
1026    if matching.is_empty() {
1027        return CascadeOutcome::Inherit;
1028    }
1029
1030    matching.sort_by_key(|declaration| std::cmp::Reverse(declaration.key));
1031    let winner = matching.remove(0);
1032    let proof = CascadeProof::from_declaration(&winner);
1033    CascadeOutcome::Definite {
1034        winner,
1035        proof,
1036        also_considered: matching,
1037    }
1038}
1039
1040pub fn run_cascade_evaluation_fuzz_case(
1041    case: CascadeEvaluationFuzzCaseV0,
1042) -> CascadeEvaluationFuzzResultV0 {
1043    let declaration_count = case.declaration_count.clamp(1, 64);
1044    let declarations = generated_cascade_fuzz_declarations(case.seed, declaration_count);
1045    let matching = declarations
1046        .iter()
1047        .filter(|declaration| declaration.property == "color")
1048        .cloned()
1049        .collect::<Vec<_>>();
1050    let expected_winner_id = rank_cascade_items(matching.clone(), |declaration| declaration.key)
1051        .first()
1052        .map(|declaration| declaration.id.clone());
1053    let actual = cascade_property(declarations, "color");
1054    let actual_winner_id = match actual {
1055        CascadeOutcome::Definite { winner, .. } => Some(winner.id),
1056        _ => None,
1057    };
1058    let ranked_count = matching.len();
1059    let passed = actual_winner_id == expected_winner_id && ranked_count > 0;
1060
1061    CascadeEvaluationFuzzResultV0 {
1062        seed: case.seed,
1063        declaration_count,
1064        actual_winner_id,
1065        expected_winner_id,
1066        ranked_count,
1067        passed,
1068    }
1069}
1070
1071pub fn run_var_substitution_fuzz_case(
1072    case: VarSubstitutionFuzzCaseV0,
1073) -> VarSubstitutionFuzzResultV0 {
1074    let chain_len = case.chain_len.clamp(1, 32);
1075    let mut env = CustomPropertyEnv::new();
1076    let terminal = CascadeValue::Literal(format!("seed-{}", case.seed));
1077
1078    for index in 0..chain_len {
1079        let name = fuzz_var_name(index);
1080        let next_value = if index == 0 && !case.cycle {
1081            terminal.clone()
1082        } else if index == 0 {
1083            CascadeValue::Var {
1084                name: fuzz_var_name(chain_len - 1),
1085                fallback: Some(Box::new(CascadeValue::Literal(
1086                    "cycle-fallback".to_string(),
1087                ))),
1088            }
1089        } else {
1090            CascadeValue::Var {
1091                name: fuzz_var_name(index - 1),
1092                fallback: None,
1093            }
1094        };
1095        env.insert(name, next_value);
1096    }
1097
1098    let input = CascadeValue::Var {
1099        name: fuzz_var_name(chain_len - 1),
1100        fallback: Some(Box::new(CascadeValue::Literal(
1101            "outer-fallback".to_string(),
1102        ))),
1103    };
1104    let result = substitute_custom_properties(&input, &env);
1105    let expected = if case.cycle {
1106        CascadeValue::GuaranteedInvalid
1107    } else {
1108        terminal
1109    };
1110    let passed = result == expected;
1111
1112    VarSubstitutionFuzzResultV0 {
1113        seed: case.seed,
1114        chain_len,
1115        cycle: case.cycle,
1116        result,
1117        expected,
1118        passed,
1119    }
1120}
1121
1122pub fn run_cascade_fuzz_seed_corpus() -> CascadeFuzzSeedReportV0 {
1123    let seeds = [0, 1, 2, 3, 5, 8, 13, 21, 34, 55, 89, 144];
1124    let cascade_results = seeds
1125        .into_iter()
1126        .enumerate()
1127        .map(|(index, seed)| {
1128            run_cascade_evaluation_fuzz_case(CascadeEvaluationFuzzCaseV0 {
1129                seed,
1130                declaration_count: index + 1,
1131            })
1132        })
1133        .collect::<Vec<_>>();
1134    let var_results = seeds
1135        .into_iter()
1136        .enumerate()
1137        .map(|(index, seed)| {
1138            run_var_substitution_fuzz_case(VarSubstitutionFuzzCaseV0 {
1139                seed,
1140                chain_len: index + 1,
1141                cycle: index % 3 == 0,
1142            })
1143        })
1144        .collect::<Vec<_>>();
1145    let passed_count = cascade_results
1146        .iter()
1147        .filter(|result| result.passed)
1148        .count()
1149        + var_results.iter().filter(|result| result.passed).count();
1150    let case_count = cascade_results.len() + var_results.len();
1151
1152    CascadeFuzzSeedReportV0 {
1153        schema_version: "0",
1154        product: "omena-cascade.fuzz-seed-corpus",
1155        case_count,
1156        passed_count,
1157        failed_count: case_count - passed_count,
1158        cascade_results,
1159        var_results,
1160    }
1161}
1162
1163pub fn rank_cascade_items<T>(
1164    items: impl IntoIterator<Item = T>,
1165    key_for: impl Fn(&T) -> CascadeKey,
1166) -> Vec<T> {
1167    let mut ranked = items.into_iter().collect::<Vec<_>>();
1168    ranked.sort_by_key(|item| Reverse(key_for(item)));
1169    ranked
1170}
1171
1172pub fn select_cascade_winner<T>(
1173    items: impl IntoIterator<Item = T>,
1174    key_for: impl Fn(&T) -> CascadeKey,
1175) -> Option<(T, Vec<T>)> {
1176    let mut ranked = rank_cascade_items(items, key_for);
1177    if ranked.is_empty() {
1178        return None;
1179    }
1180
1181    let winner = ranked.remove(0);
1182    Some((winner, ranked))
1183}
1184
1185pub fn prove_box_shorthand_combination(
1186    shorthand_property: &str,
1187    longhands: &[BoxLonghandInputV0],
1188) -> ShorthandCombinationProofV0 {
1189    let expected = match box_shorthand_longhands(shorthand_property) {
1190        Some(expected) => expected,
1191        None => {
1192            return shorthand_combination_proof(
1193                shorthand_property,
1194                false,
1195                Some("unsupported shorthand property"),
1196                longhands,
1197                "",
1198            );
1199        }
1200    };
1201
1202    if longhands.len() != expected.len() {
1203        return shorthand_combination_proof(
1204            shorthand_property,
1205            false,
1206            Some("incomplete longhand quartet"),
1207            longhands,
1208            "",
1209        );
1210    }
1211
1212    if longhands
1213        .iter()
1214        .zip(expected.iter())
1215        .any(|(actual, expected)| actual.property != *expected)
1216    {
1217        return shorthand_combination_proof(
1218            shorthand_property,
1219            false,
1220            Some("longhands are not in canonical top/right/bottom/left order"),
1221            longhands,
1222            "",
1223        );
1224    }
1225
1226    if longhands.iter().any(|longhand| longhand.important) {
1227        return shorthand_combination_proof(
1228            shorthand_property,
1229            false,
1230            Some("important longhands require explicit cascade equivalence proof"),
1231            longhands,
1232            "",
1233        );
1234    }
1235
1236    if longhands.iter().any(|longhand| longhand.value.is_empty()) {
1237        return shorthand_combination_proof(
1238            shorthand_property,
1239            false,
1240            Some("empty longhand value"),
1241            longhands,
1242            "",
1243        );
1244    }
1245
1246    if longhands
1247        .windows(2)
1248        .any(|pair| pair[1].source_order != pair[0].source_order + 1)
1249    {
1250        return shorthand_combination_proof(
1251            shorthand_property,
1252            false,
1253            Some("intervening declaration may change cascade outcome"),
1254            longhands,
1255            "",
1256        );
1257    }
1258
1259    shorthand_combination_proof(
1260        shorthand_property,
1261        true,
1262        None,
1263        longhands,
1264        "all four longhands are adjacent, non-important, and in canonical order",
1265    )
1266}
1267
1268pub fn evaluate_static_supports_condition(
1269    condition: &str,
1270    assumption: StaticSupportsAssumptionV0,
1271) -> StaticSupportsEvalWitnessV0 {
1272    let normalized_condition = normalize_ascii_whitespace(condition);
1273    let (verdict, reason) = match assumption {
1274        StaticSupportsAssumptionV0::ModernBrowser => {
1275            if parse_not_simple_supports_declaration(&normalized_condition).is_some() {
1276                (
1277                    StaticSupportsEvalVerdictV0::AlwaysFalse,
1278                    "modern-browser assumption rejects negated simple declaration feature queries",
1279                )
1280            } else if parse_simple_supports_declaration(&normalized_condition).is_some() {
1281                (
1282                    StaticSupportsEvalVerdictV0::AlwaysTrue,
1283                    "modern-browser assumption accepts simple declaration feature queries",
1284                )
1285            } else {
1286                (
1287                    StaticSupportsEvalVerdictV0::Unknown,
1288                    "unsupported supports condition shape",
1289                )
1290            }
1291        }
1292    };
1293
1294    StaticSupportsEvalWitnessV0 {
1295        schema_version: "0",
1296        product: "omena-cascade.supports-static-eval",
1297        condition: normalized_condition,
1298        assumption,
1299        verdict,
1300        reason,
1301        provenance_preserved: verdict != StaticSupportsEvalVerdictV0::Unknown,
1302    }
1303}
1304
1305pub fn prove_scope_flatten_candidate(input: ScopeFlattenInputV0) -> ScopeFlattenProofV0 {
1306    let blocked_reason = if input.limit_selector.is_some() {
1307        Some("scope limit selector cannot be encoded by the conservative flatten predicate")
1308    } else if input.root_selector.trim() != ":root" {
1309        Some("non-root scope flattening requires selector/proximity equivalence proof")
1310    } else if input.peer_scope_count > 0 {
1311        Some("peer scopes may change scope-proximity cascade ordering")
1312    } else if input.competing_unscoped_rule_count > 0 {
1313        Some("unscoped competitors may observe changed scope-proximity ordering")
1314    } else if input.inside_layer {
1315        Some("layer plus scope composition requires product cascade proof")
1316    } else {
1317        None
1318    };
1319    let accepted = blocked_reason.is_none();
1320    ScopeFlattenProofV0 {
1321        schema_version: "0",
1322        product: "omena-cascade.scope-flatten-proof",
1323        accepted,
1324        blocked_reason,
1325        root_selector: input.root_selector,
1326        provenance_preserved: accepted,
1327        cascade_safe_witness: if accepted {
1328            "root scope without limit, peer scopes, unscoped competition, or layer context"
1329        } else {
1330            "scope proximity cannot be erased by local syntax alone"
1331        }
1332        .to_string(),
1333    }
1334}
1335
1336pub fn prove_layer_flatten_candidate(input: LayerFlattenInputV0) -> LayerFlattenProofV0 {
1337    let blocked_reason = if !input.closed_bundle {
1338        Some("layer flattening requires a closed bundle witness")
1339    } else if input.peer_layer_count > 0 {
1340        Some("peer layers may change layer-rank cascade ordering")
1341    } else if input.unlayered_rule_count > 0 {
1342        Some("unlayered rules compete differently from layered normal rules")
1343    } else if input.important_declaration_count > 0 {
1344        Some("important declarations invert layer ordering")
1345    } else {
1346        None
1347    };
1348    let accepted = blocked_reason.is_none();
1349    LayerFlattenProofV0 {
1350        schema_version: "0",
1351        product: "omena-cascade.layer-flatten-proof",
1352        accepted,
1353        blocked_reason,
1354        layer_name: input.layer_name,
1355        provenance_preserved: accepted,
1356        cascade_safe_witness: if accepted {
1357            "closed bundle with a single non-important layer and no unlayered competitors"
1358        } else {
1359            "layer rank cannot be erased by local syntax alone"
1360        }
1361        .to_string(),
1362    }
1363}
1364
1365fn parse_not_simple_supports_declaration(condition: &str) -> Option<(&str, &str)> {
1366    let inner = condition.strip_prefix("not ")?;
1367    parse_simple_supports_declaration(inner)
1368}
1369
1370fn parse_simple_supports_declaration(condition: &str) -> Option<(&str, &str)> {
1371    let inner = condition.strip_prefix('(')?.strip_suffix(')')?.trim();
1372    let (property, value) = inner.split_once(':')?;
1373    let property = property.trim();
1374    let value = value.trim();
1375    if property.is_empty()
1376        || value.is_empty()
1377        || property.contains(|ch: char| !is_supports_declaration_token_char(ch))
1378        || value.contains(['{', '}', ';', '(', ')'])
1379    {
1380        return None;
1381    }
1382    Some((property, value))
1383}
1384
1385fn is_supports_declaration_token_char(ch: char) -> bool {
1386    ch.is_ascii_alphanumeric() || matches!(ch, '-' | '_')
1387}
1388
1389fn normalize_ascii_whitespace(text: &str) -> String {
1390    text.split_whitespace().collect::<Vec<_>>().join(" ")
1391}
1392
1393fn box_shorthand_longhands(shorthand_property: &str) -> Option<[&'static str; 4]> {
1394    match shorthand_property {
1395        "margin" => Some(["margin-top", "margin-right", "margin-bottom", "margin-left"]),
1396        "padding" => Some([
1397            "padding-top",
1398            "padding-right",
1399            "padding-bottom",
1400            "padding-left",
1401        ]),
1402        _ => None,
1403    }
1404}
1405
1406fn shorthand_combination_proof(
1407    shorthand_property: &str,
1408    accepted: bool,
1409    blocked_reason: Option<&'static str>,
1410    longhands: &[BoxLonghandInputV0],
1411    witness: &str,
1412) -> ShorthandCombinationProofV0 {
1413    ShorthandCombinationProofV0 {
1414        schema_version: "0",
1415        product: "omena-cascade.shorthand-combination-proof",
1416        shorthand_property: shorthand_property.to_string(),
1417        accepted,
1418        blocked_reason,
1419        ordered_longhand_properties: longhands
1420            .iter()
1421            .map(|longhand| longhand.property.clone())
1422            .collect(),
1423        provenance_preserved: accepted,
1424        cascade_safe_witness: witness.to_string(),
1425    }
1426}
1427
1428pub fn selector_context_witness(
1429    declaration_selectors: &[String],
1430    reference_selectors: &[String],
1431) -> SelectorContextWitness {
1432    if declaration_selectors.is_empty() {
1433        return SelectorContextWitness {
1434            kind: SelectorContextMatchKind::Global,
1435            matched: true,
1436            rank: 1,
1437            declaration_selector: None,
1438            reference_selector: None,
1439        };
1440    }
1441
1442    let mut best = SelectorContextWitness::no_match();
1443    for declaration_selector in declaration_selectors {
1444        let candidate = selector_context_witness_for_declaration(
1445            declaration_selector.as_str(),
1446            reference_selectors,
1447        );
1448        if candidate.rank > best.rank {
1449            best = candidate;
1450        }
1451    }
1452    best
1453}
1454
1455pub fn selector_context_witness_for_declaration(
1456    declaration_selector: &str,
1457    reference_selectors: &[String],
1458) -> SelectorContextWitness {
1459    if declaration_selector == ":root" {
1460        return SelectorContextWitness {
1461            kind: SelectorContextMatchKind::Root,
1462            matched: true,
1463            rank: 1,
1464            declaration_selector: Some(declaration_selector.to_string()),
1465            reference_selector: None,
1466        };
1467    }
1468
1469    for reference_selector in reference_selectors {
1470        if reference_selector == declaration_selector {
1471            return SelectorContextWitness {
1472                kind: SelectorContextMatchKind::Exact,
1473                matched: true,
1474                rank: 2,
1475                declaration_selector: Some(declaration_selector.to_string()),
1476                reference_selector: Some(reference_selector.clone()),
1477            };
1478        }
1479    }
1480
1481    for reference_selector in reference_selectors {
1482        if reference_selector.contains(declaration_selector) {
1483            return SelectorContextWitness {
1484                kind: SelectorContextMatchKind::ContainsSelector,
1485                matched: true,
1486                rank: 2,
1487                declaration_selector: Some(declaration_selector.to_string()),
1488                reference_selector: Some(reference_selector.clone()),
1489            };
1490        }
1491    }
1492
1493    SelectorContextWitness {
1494        kind: SelectorContextMatchKind::NoMatch,
1495        matched: false,
1496        rank: 0,
1497        declaration_selector: Some(declaration_selector.to_string()),
1498        reference_selector: None,
1499    }
1500}
1501
1502pub fn selector_match_witness(selector: &str, element: &ElementSignature) -> SelectorMatchWitness {
1503    let branches = split_selector_list(selector);
1504    if branches.is_empty() {
1505        return SelectorMatchWitness::unsupported(selector);
1506    }
1507
1508    let mut witnesses = branches
1509        .iter()
1510        .map(|branch| selector_match_branch_witness(branch, element))
1511        .collect::<Vec<_>>();
1512
1513    let yes = strongest_by_verdict(&witnesses, SelectorMatchVerdict::Yes);
1514    if let Some(index) = yes {
1515        let mut witness = witnesses.remove(index);
1516        witness.selector = selector.to_string();
1517        if branches.len() > 1 {
1518            witness.reason = SelectorMatchReason::SelectorList;
1519            witness.unsupported_branches = witnesses
1520                .into_iter()
1521                .flat_map(|witness| witness.unsupported_branches)
1522                .collect();
1523        }
1524        return witness;
1525    }
1526
1527    let maybe = strongest_by_verdict(&witnesses, SelectorMatchVerdict::Maybe);
1528    if let Some(index) = maybe {
1529        let mut witness = witnesses.remove(index);
1530        witness.selector = selector.to_string();
1531        if branches.len() > 1 {
1532            witness.reason = SelectorMatchReason::SelectorList;
1533            witness.unsupported_branches = witnesses
1534                .into_iter()
1535                .flat_map(|witness| witness.unsupported_branches)
1536                .collect();
1537        }
1538        return witness;
1539    }
1540
1541    let mut witness = witnesses
1542        .into_iter()
1543        .max_by(|left, right| left.specificity.cmp(&right.specificity))
1544        .unwrap_or_else(|| SelectorMatchWitness::unsupported(selector));
1545    witness.selector = selector.to_string();
1546    if branches.len() > 1 {
1547        witness.reason = SelectorMatchReason::SelectorList;
1548    }
1549    witness
1550}
1551
1552pub fn parse_simple_selector_signature(selector: &str) -> Option<SelectorSignature> {
1553    parse_simple_selector_signature_inner(selector.trim())
1554}
1555
1556fn selector_match_branch_witness(
1557    selector: &str,
1558    element: &ElementSignature,
1559) -> SelectorMatchWitness {
1560    let Some(signature) = parse_simple_selector_signature(selector) else {
1561        return SelectorMatchWitness::unsupported(selector);
1562    };
1563
1564    let mut witness = SelectorMatchWitness {
1565        selector: selector.to_string(),
1566        matched_branch: Some(selector.to_string()),
1567        verdict: SelectorMatchVerdict::Yes,
1568        reason: if signature.required_tag.is_none()
1569            && signature.required_id.is_none()
1570            && signature.required_classes.is_empty()
1571            && signature.required_attributes.is_empty()
1572            && signature.required_pseudo_states.is_empty()
1573        {
1574            SelectorMatchReason::Universal
1575        } else {
1576            SelectorMatchReason::SimpleCompound
1577        },
1578        specificity: signature.specificity,
1579        missing_tag: None,
1580        missing_id: None,
1581        missing_classes: BTreeSet::new(),
1582        missing_attributes: BTreeSet::new(),
1583        missing_pseudo_states: BTreeSet::new(),
1584        unsupported_branches: Vec::new(),
1585    };
1586
1587    if let Some(required_tag) = &signature.required_tag {
1588        match element.tag.as_deref() {
1589            Some(tag) if tag == required_tag => {}
1590            _ if !element.tag_is_exact => {
1591                witness.verdict = SelectorMatchVerdict::Maybe;
1592                witness.reason = SelectorMatchReason::MissingTag;
1593                witness.missing_tag = Some(required_tag.clone());
1594            }
1595            _ => {
1596                witness.verdict = SelectorMatchVerdict::No;
1597                witness.reason = SelectorMatchReason::MissingTag;
1598                witness.missing_tag = Some(required_tag.clone());
1599            }
1600        }
1601    }
1602
1603    if let Some(required_id) = &signature.required_id {
1604        match element.id.as_deref() {
1605            Some(id) if id == required_id => {}
1606            _ if !element.id_is_exact && witness.verdict != SelectorMatchVerdict::No => {
1607                witness.verdict = SelectorMatchVerdict::Maybe;
1608                witness.reason = SelectorMatchReason::MissingId;
1609                witness.missing_id = Some(required_id.clone());
1610            }
1611            _ => {
1612                witness.verdict = SelectorMatchVerdict::No;
1613                witness.reason = SelectorMatchReason::MissingId;
1614                witness.missing_id = Some(required_id.clone());
1615            }
1616        }
1617    }
1618
1619    for required_class in &signature.required_classes {
1620        if element.classes.contains(required_class) {
1621            continue;
1622        }
1623        if !element.classes_are_exact && witness.verdict != SelectorMatchVerdict::No {
1624            witness.verdict = SelectorMatchVerdict::Maybe;
1625        } else {
1626            witness.verdict = SelectorMatchVerdict::No;
1627        }
1628        witness.reason = SelectorMatchReason::MissingClass;
1629        witness.missing_classes.insert(required_class.clone());
1630    }
1631
1632    for required_attribute in &signature.required_attributes {
1633        if element.attributes.contains(required_attribute) {
1634            continue;
1635        }
1636        if !element.attributes_are_exact && witness.verdict != SelectorMatchVerdict::No {
1637            witness.verdict = SelectorMatchVerdict::Maybe;
1638        } else {
1639            witness.verdict = SelectorMatchVerdict::No;
1640        }
1641        witness.reason = SelectorMatchReason::MissingAttribute;
1642        witness
1643            .missing_attributes
1644            .insert(required_attribute.clone());
1645    }
1646
1647    for required_pseudo_state in &signature.required_pseudo_states {
1648        if element.pseudo_states.contains(required_pseudo_state) {
1649            continue;
1650        }
1651        if !element.pseudo_states_are_exact && witness.verdict != SelectorMatchVerdict::No {
1652            witness.verdict = SelectorMatchVerdict::Maybe;
1653        } else {
1654            witness.verdict = SelectorMatchVerdict::No;
1655        }
1656        witness.reason = SelectorMatchReason::MissingPseudoState;
1657        witness
1658            .missing_pseudo_states
1659            .insert(required_pseudo_state.clone());
1660    }
1661
1662    witness
1663}
1664
1665fn strongest_by_verdict(
1666    witnesses: &[SelectorMatchWitness],
1667    verdict: SelectorMatchVerdict,
1668) -> Option<usize> {
1669    witnesses
1670        .iter()
1671        .enumerate()
1672        .filter(|(_, witness)| witness.verdict == verdict)
1673        .max_by(|(_, left), (_, right)| left.specificity.cmp(&right.specificity))
1674        .map(|(index, _)| index)
1675}
1676
1677fn parse_simple_selector_signature_inner(selector: &str) -> Option<SelectorSignature> {
1678    if selector.is_empty() || selector_has_unsupported_top_level_syntax(selector) {
1679        return None;
1680    }
1681
1682    let mut required_tag = None;
1683    let mut required_id = None;
1684    let mut required_classes = BTreeSet::new();
1685    let mut required_attributes = BTreeSet::new();
1686    let mut required_pseudo_states = BTreeSet::new();
1687    let mut specificity = Specificity::ZERO;
1688    let chars = selector.chars().collect::<Vec<_>>();
1689    let mut index = 0;
1690
1691    while index < chars.len() {
1692        match chars[index] {
1693            '*' => index += 1,
1694            '.' => {
1695                index += 1;
1696                let (name, next) = read_identifier(&chars, index)?;
1697                specificity.classes += 1;
1698                required_classes.insert(name);
1699                index = next;
1700            }
1701            '#' => {
1702                index += 1;
1703                let (name, next) = read_identifier(&chars, index)?;
1704                specificity.ids += 1;
1705                required_id = Some(name);
1706                index = next;
1707            }
1708            '[' => {
1709                let close = find_closing_bracket(&chars, index)?;
1710                let attribute = chars[index + 1..close].iter().collect::<String>();
1711                let attribute_name = read_attribute_name(attribute.trim())?;
1712                specificity.classes += 1;
1713                required_attributes.insert(attribute_name);
1714                index = close + 1;
1715            }
1716            ':' => {
1717                if matches!(chars.get(index + 1), Some(':')) {
1718                    index += 2;
1719                    let (_, next) = read_identifier(&chars, index)?;
1720                    specificity.elements += 1;
1721                    index = next;
1722                } else {
1723                    index += 1;
1724                    let (name, next) = read_identifier(&chars, index)?;
1725                    if matches!(chars.get(next), Some('(')) {
1726                        return None;
1727                    }
1728                    specificity.classes += 1;
1729                    required_pseudo_states.insert(name);
1730                    index = next;
1731                }
1732            }
1733            ch if is_identifier_start(ch) => {
1734                let (name, next) = read_identifier(&chars, index)?;
1735                if required_tag.is_some() {
1736                    return None;
1737                }
1738                specificity.elements += 1;
1739                required_tag = Some(name);
1740                index = next;
1741            }
1742            _ => return None,
1743        }
1744    }
1745
1746    Some(SelectorSignature {
1747        selector: selector.to_string(),
1748        required_tag,
1749        required_id,
1750        required_classes,
1751        required_attributes,
1752        required_pseudo_states,
1753        specificity,
1754    })
1755}
1756
1757fn split_selector_list(selector: &str) -> Vec<String> {
1758    let mut branches = Vec::new();
1759    let mut start = 0;
1760    let mut paren_depth: usize = 0;
1761    let mut bracket_depth: usize = 0;
1762    let chars = selector.char_indices().collect::<Vec<_>>();
1763
1764    for (index, ch) in &chars {
1765        match *ch {
1766            '(' => paren_depth += 1,
1767            ')' => paren_depth = paren_depth.saturating_sub(1),
1768            '[' => bracket_depth += 1,
1769            ']' => bracket_depth = bracket_depth.saturating_sub(1),
1770            ',' if paren_depth == 0 && bracket_depth == 0 => {
1771                let branch = selector[start..*index].trim();
1772                if !branch.is_empty() {
1773                    branches.push(branch.to_string());
1774                }
1775                start = *index + 1;
1776            }
1777            _ => {}
1778        }
1779    }
1780
1781    let tail = selector[start..].trim();
1782    if !tail.is_empty() {
1783        branches.push(tail.to_string());
1784    }
1785    branches
1786}
1787
1788fn selector_has_unsupported_top_level_syntax(selector: &str) -> bool {
1789    let mut paren_depth: usize = 0;
1790    let mut bracket_depth: usize = 0;
1791    for ch in selector.chars() {
1792        match ch {
1793            '(' => paren_depth += 1,
1794            ')' => paren_depth = paren_depth.saturating_sub(1),
1795            '[' => bracket_depth += 1,
1796            ']' => bracket_depth = bracket_depth.saturating_sub(1),
1797            '>' | '+' | '~' if paren_depth == 0 && bracket_depth == 0 => return true,
1798            ch if ch.is_whitespace() && paren_depth == 0 && bracket_depth == 0 => return true,
1799            _ => {}
1800        }
1801    }
1802    false
1803}
1804
1805fn find_closing_bracket(chars: &[char], open_index: usize) -> Option<usize> {
1806    chars
1807        .iter()
1808        .enumerate()
1809        .skip(open_index + 1)
1810        .find_map(|(index, ch)| if *ch == ']' { Some(index) } else { None })
1811}
1812
1813fn read_attribute_name(attribute: &str) -> Option<String> {
1814    let name = attribute
1815        .split(|ch: char| ch.is_whitespace() || matches!(ch, '=' | '~' | '|' | '^' | '$' | '*'))
1816        .find(|part| !part.is_empty())?;
1817    Some(name.to_string())
1818}
1819
1820fn read_identifier(chars: &[char], start: usize) -> Option<(String, usize)> {
1821    if start >= chars.len() || !is_identifier_start(chars[start]) {
1822        return None;
1823    }
1824    let mut end = start + 1;
1825    while end < chars.len() && is_identifier_continue(chars[end]) {
1826        end += 1;
1827    }
1828    Some((chars[start..end].iter().collect(), end))
1829}
1830
1831fn is_identifier_start(ch: char) -> bool {
1832    ch.is_ascii_alphabetic() || matches!(ch, '_' | '-')
1833}
1834
1835fn is_identifier_continue(ch: char) -> bool {
1836    ch.is_ascii_alphanumeric() || matches!(ch, '_' | '-')
1837}
1838
1839pub fn substitute_custom_properties(value: &CascadeValue, env: &CustomPropertyEnv) -> CascadeValue {
1840    let mut visiting = BTreeSet::new();
1841    substitute_custom_properties_inner(value, env, &mut visiting)
1842}
1843
1844fn substitute_custom_properties_inner(
1845    value: &CascadeValue,
1846    env: &CustomPropertyEnv,
1847    visiting: &mut BTreeSet<String>,
1848) -> CascadeValue {
1849    match value {
1850        CascadeValue::Literal(_) | CascadeValue::GuaranteedInvalid | CascadeValue::Unset => {
1851            value.clone()
1852        }
1853        CascadeValue::Var { name, fallback } => {
1854            if !visiting.insert(name.clone()) {
1855                return CascadeValue::GuaranteedInvalid;
1856            }
1857            let resolved = match env.get(name) {
1858                Some(CascadeValue::Unset) | None => fallback
1859                    .as_deref()
1860                    .map(|fallback| substitute_custom_properties_inner(fallback, env, visiting))
1861                    .unwrap_or(CascadeValue::GuaranteedInvalid),
1862                Some(value) => substitute_custom_properties_inner(value, env, visiting),
1863            };
1864            visiting.remove(name);
1865            resolved
1866        }
1867    }
1868}
1869
1870fn generated_cascade_fuzz_declarations(
1871    seed: u64,
1872    declaration_count: usize,
1873) -> Vec<CascadeDeclaration> {
1874    let mut state = seed ^ 0x9e37_79b9_7f4a_7c15;
1875    (0..declaration_count)
1876        .map(|index| {
1877            let property = if index == 0 || fuzz_next(&mut state).is_multiple_of(3) {
1878                "color"
1879            } else {
1880                "margin"
1881            };
1882            CascadeDeclaration {
1883                id: format!("decl-{seed}-{index}"),
1884                property: property.to_string(),
1885                value: CascadeValue::Literal(format!("v{}", fuzz_next(&mut state) % 17)),
1886                key: CascadeKey::new(
1887                    fuzz_cascade_level(fuzz_next(&mut state)),
1888                    LayerRank((fuzz_next(&mut state) % 9) as i32 - 4),
1889                    (fuzz_next(&mut state) % 12) as u32,
1890                    Specificity::new(
1891                        (fuzz_next(&mut state) % 4) as u32,
1892                        (fuzz_next(&mut state) % 8) as u32,
1893                        (fuzz_next(&mut state) % 12) as u32,
1894                    ),
1895                    index as u32,
1896                ),
1897            }
1898        })
1899        .collect()
1900}
1901
1902fn fuzz_cascade_level(value: u64) -> CascadeLevel {
1903    match value % 9 {
1904        0 => CascadeLevel::UserAgentNormal,
1905        1 => CascadeLevel::UserNormal,
1906        2 => CascadeLevel::AuthorNormal,
1907        3 => CascadeLevel::InlineNormal,
1908        4 => CascadeLevel::Animation,
1909        5 => CascadeLevel::AuthorImportant,
1910        6 => CascadeLevel::UserImportant,
1911        7 => CascadeLevel::UserAgentImportant,
1912        _ => CascadeLevel::Transition,
1913    }
1914}
1915
1916fn fuzz_var_name(index: usize) -> String {
1917    format!("--fuzz-{index}")
1918}
1919
1920fn fuzz_next(state: &mut u64) -> u64 {
1921    *state = state
1922        .wrapping_mul(6_364_136_223_846_793_005)
1923        .wrapping_add(1_442_695_040_888_963_407);
1924    *state
1925}
1926
1927#[cfg(test)]
1928mod tests {
1929    use super::*;
1930
1931    fn declaration(id: &str, value: &str, key: CascadeKey) -> CascadeDeclaration {
1932        CascadeDeclaration {
1933            id: id.to_string(),
1934            property: "color".to_string(),
1935            value: CascadeValue::Literal(value.to_string()),
1936            key,
1937        }
1938    }
1939
1940    fn key(
1941        level: CascadeLevel,
1942        layer_rank: i32,
1943        scope_proximity: u32,
1944        specificity: Specificity,
1945        source_order: u32,
1946    ) -> CascadeKey {
1947        CascadeKey::new(
1948            level,
1949            LayerRank(layer_rank),
1950            scope_proximity,
1951            specificity,
1952            source_order,
1953        )
1954    }
1955
1956    #[test]
1957    fn orders_specificity_lexicographically() {
1958        assert!(Specificity::new(1, 0, 0) > Specificity::new(0, 99, 99));
1959        assert!(Specificity::new(0, 2, 0) > Specificity::new(0, 1, 99));
1960        assert!(Specificity::new(0, 0, 2) > Specificity::new(0, 0, 1));
1961    }
1962
1963    #[test]
1964    fn orders_cascade_keys_by_level_layer_scope_specificity_and_source() {
1965        let base = key(
1966            CascadeLevel::AuthorNormal,
1967            0,
1968            3,
1969            Specificity::new(0, 1, 0),
1970            1,
1971        );
1972        assert!(
1973            key(
1974                CascadeLevel::AuthorImportant,
1975                0,
1976                3,
1977                Specificity::new(0, 1, 0),
1978                1,
1979            ) > base
1980        );
1981        assert!(
1982            key(
1983                CascadeLevel::AuthorNormal,
1984                1,
1985                3,
1986                Specificity::new(0, 1, 0),
1987                1,
1988            ) > base
1989        );
1990        assert!(
1991            key(
1992                CascadeLevel::AuthorNormal,
1993                0,
1994                1,
1995                Specificity::new(0, 1, 0),
1996                1,
1997            ) > base
1998        );
1999        assert!(
2000            key(
2001                CascadeLevel::AuthorNormal,
2002                0,
2003                3,
2004                Specificity::new(0, 2, 0),
2005                1,
2006            ) > base
2007        );
2008        assert!(
2009            key(
2010                CascadeLevel::AuthorNormal,
2011                0,
2012                3,
2013                Specificity::new(0, 1, 0),
2014                2,
2015            ) > base
2016        );
2017    }
2018
2019    #[test]
2020    fn selects_definite_winner_with_proof() {
2021        let earlier = declaration(
2022            "earlier",
2023            "red",
2024            key(
2025                CascadeLevel::AuthorNormal,
2026                0,
2027                1,
2028                Specificity::new(0, 1, 0),
2029                1,
2030            ),
2031        );
2032        let later = declaration(
2033            "later",
2034            "blue",
2035            key(
2036                CascadeLevel::AuthorNormal,
2037                0,
2038                1,
2039                Specificity::new(0, 1, 0),
2040                2,
2041            ),
2042        );
2043
2044        let outcome = cascade_property([earlier, later], "color");
2045
2046        assert!(matches!(outcome, CascadeOutcome::Definite { .. }));
2047        if let CascadeOutcome::Definite {
2048            winner,
2049            proof,
2050            also_considered,
2051        } = outcome
2052        {
2053            assert_eq!(winner.id, "later");
2054            assert_eq!(proof.declaration_id, "later");
2055            assert_eq!(also_considered.len(), 1);
2056        }
2057    }
2058
2059    #[test]
2060    fn selects_generic_winner_with_same_cascade_ordering() {
2061        let ranked = select_cascade_winner(["earlier", "later"], |item| match *item {
2062            "earlier" => key(
2063                CascadeLevel::AuthorNormal,
2064                0,
2065                1,
2066                Specificity::new(0, 1, 0),
2067                1,
2068            ),
2069            _ => key(
2070                CascadeLevel::AuthorNormal,
2071                0,
2072                1,
2073                Specificity::new(0, 1, 0),
2074                2,
2075            ),
2076        });
2077
2078        let Some((winner, also_considered)) = ranked else {
2079            unreachable!("test input contains candidates")
2080        };
2081        assert_eq!(winner, "later");
2082        assert_eq!(also_considered, vec!["earlier"]);
2083    }
2084
2085    #[test]
2086    fn proves_adjacent_box_longhands_can_combine_to_shorthand() {
2087        let proof = prove_box_shorthand_combination(
2088            "margin",
2089            &[
2090                BoxLonghandInputV0 {
2091                    property: "margin-top".to_string(),
2092                    value: "1px".to_string(),
2093                    important: false,
2094                    source_order: 1,
2095                },
2096                BoxLonghandInputV0 {
2097                    property: "margin-right".to_string(),
2098                    value: "2px".to_string(),
2099                    important: false,
2100                    source_order: 2,
2101                },
2102                BoxLonghandInputV0 {
2103                    property: "margin-bottom".to_string(),
2104                    value: "3px".to_string(),
2105                    important: false,
2106                    source_order: 3,
2107                },
2108                BoxLonghandInputV0 {
2109                    property: "margin-left".to_string(),
2110                    value: "4px".to_string(),
2111                    important: false,
2112                    source_order: 4,
2113                },
2114            ],
2115        );
2116
2117        assert_eq!(proof.product, "omena-cascade.shorthand-combination-proof");
2118        assert!(proof.accepted);
2119        assert_eq!(proof.blocked_reason, None);
2120        assert!(proof.provenance_preserved);
2121        assert!(proof.cascade_safe_witness.contains("canonical order"));
2122    }
2123
2124    #[test]
2125    fn blocks_box_shorthand_combination_when_intervening_order_is_possible() {
2126        let proof = prove_box_shorthand_combination(
2127            "padding",
2128            &[
2129                BoxLonghandInputV0 {
2130                    property: "padding-top".to_string(),
2131                    value: "1px".to_string(),
2132                    important: false,
2133                    source_order: 1,
2134                },
2135                BoxLonghandInputV0 {
2136                    property: "padding-right".to_string(),
2137                    value: "2px".to_string(),
2138                    important: false,
2139                    source_order: 3,
2140                },
2141                BoxLonghandInputV0 {
2142                    property: "padding-bottom".to_string(),
2143                    value: "3px".to_string(),
2144                    important: false,
2145                    source_order: 4,
2146                },
2147                BoxLonghandInputV0 {
2148                    property: "padding-left".to_string(),
2149                    value: "4px".to_string(),
2150                    important: false,
2151                    source_order: 5,
2152                },
2153            ],
2154        );
2155
2156        assert!(!proof.accepted);
2157        assert_eq!(
2158            proof.blocked_reason,
2159            Some("intervening declaration may change cascade outcome")
2160        );
2161        assert!(!proof.provenance_preserved);
2162    }
2163
2164    #[test]
2165    fn evaluates_simple_supports_conditions_under_modern_browser_assumption() {
2166        let positive = evaluate_static_supports_condition(
2167            "(display: grid)",
2168            StaticSupportsAssumptionV0::ModernBrowser,
2169        );
2170        assert_eq!(positive.product, "omena-cascade.supports-static-eval");
2171        assert_eq!(positive.verdict, StaticSupportsEvalVerdictV0::AlwaysTrue);
2172        assert!(positive.provenance_preserved);
2173
2174        let negative = evaluate_static_supports_condition(
2175            "not (display: grid)",
2176            StaticSupportsAssumptionV0::ModernBrowser,
2177        );
2178        assert_eq!(negative.verdict, StaticSupportsEvalVerdictV0::AlwaysFalse);
2179        assert!(negative.provenance_preserved);
2180
2181        let unknown = evaluate_static_supports_condition(
2182            "(display: grid) and (color: red)",
2183            StaticSupportsAssumptionV0::ModernBrowser,
2184        );
2185        assert_eq!(unknown.verdict, StaticSupportsEvalVerdictV0::Unknown);
2186        assert!(!unknown.provenance_preserved);
2187    }
2188
2189    #[test]
2190    fn proves_only_root_scope_flatten_candidates_without_competition() {
2191        let accepted = prove_scope_flatten_candidate(ScopeFlattenInputV0 {
2192            root_selector: ":root".to_string(),
2193            limit_selector: None,
2194            scoped_rule_count: 1,
2195            peer_scope_count: 0,
2196            competing_unscoped_rule_count: 0,
2197            inside_layer: false,
2198        });
2199        assert_eq!(accepted.product, "omena-cascade.scope-flatten-proof");
2200        assert!(accepted.accepted);
2201        assert!(accepted.provenance_preserved);
2202
2203        let blocked = prove_scope_flatten_candidate(ScopeFlattenInputV0 {
2204            root_selector: ".card".to_string(),
2205            limit_selector: None,
2206            scoped_rule_count: 1,
2207            peer_scope_count: 0,
2208            competing_unscoped_rule_count: 0,
2209            inside_layer: false,
2210        });
2211        assert!(!blocked.accepted);
2212        assert_eq!(
2213            blocked.blocked_reason,
2214            Some("non-root scope flattening requires selector/proximity equivalence proof")
2215        );
2216    }
2217
2218    #[test]
2219    fn proves_layer_flatten_only_for_closed_single_layer_candidates() {
2220        let accepted = prove_layer_flatten_candidate(LayerFlattenInputV0 {
2221            layer_name: Some("theme".to_string()),
2222            layer_rule_count: 1,
2223            peer_layer_count: 0,
2224            unlayered_rule_count: 0,
2225            important_declaration_count: 0,
2226            closed_bundle: true,
2227        });
2228        assert_eq!(accepted.product, "omena-cascade.layer-flatten-proof");
2229        assert!(accepted.accepted);
2230        assert!(accepted.provenance_preserved);
2231
2232        let blocked = prove_layer_flatten_candidate(LayerFlattenInputV0 {
2233            layer_name: Some("theme".to_string()),
2234            layer_rule_count: 1,
2235            peer_layer_count: 0,
2236            unlayered_rule_count: 1,
2237            important_declaration_count: 0,
2238            closed_bundle: true,
2239        });
2240        assert!(!blocked.accepted);
2241        assert_eq!(
2242            blocked.blocked_reason,
2243            Some("unlayered rules compete differently from layered normal rules")
2244        );
2245    }
2246
2247    #[test]
2248    fn reports_selector_context_witness_rank() {
2249        let root = selector_context_witness(&[":root".to_string()], &[".button".to_string()]);
2250        assert_eq!(root.kind, SelectorContextMatchKind::Root);
2251        assert!(root.matched);
2252        assert_eq!(root.rank, 1);
2253
2254        let exact = selector_context_witness(&[".button".to_string()], &[".button".to_string()]);
2255        assert_eq!(exact.kind, SelectorContextMatchKind::Exact);
2256        assert_eq!(exact.rank, 2);
2257
2258        let descendant =
2259            selector_context_witness(&[".theme".to_string()], &[".theme .button".to_string()]);
2260        assert_eq!(descendant.kind, SelectorContextMatchKind::ContainsSelector);
2261        assert_eq!(
2262            descendant.reference_selector.as_deref(),
2263            Some(".theme .button")
2264        );
2265
2266        let miss = selector_context_witness(&[".card".to_string()], &[".button".to_string()]);
2267        assert_eq!(miss.kind, SelectorContextMatchKind::NoMatch);
2268        assert!(!miss.matched);
2269    }
2270
2271    #[test]
2272    fn parses_simple_selector_specificity() {
2273        let signature = parse_simple_selector_signature("button#save.primary[data-state]:hover");
2274        assert!(signature.is_some());
2275        if let Some(signature) = signature {
2276            assert_eq!(signature.required_tag.as_deref(), Some("button"));
2277            assert_eq!(signature.required_id.as_deref(), Some("save"));
2278            assert!(signature.required_classes.contains("primary"));
2279            assert!(signature.required_attributes.contains("data-state"));
2280            assert!(signature.required_pseudo_states.contains("hover"));
2281            assert_eq!(signature.specificity, Specificity::new(1, 3, 1));
2282        }
2283    }
2284
2285    #[test]
2286    fn matches_simple_compound_selectors_against_concrete_signature() {
2287        let mut element =
2288            ElementSignature::concrete(Some("button"), Some("save"), ["primary", "active"]);
2289        element.attributes.insert("data-state".to_string());
2290        element.pseudo_states.insert("hover".to_string());
2291
2292        let witness = selector_match_witness("button#save.primary[data-state]:hover", &element);
2293
2294        assert_eq!(witness.verdict, SelectorMatchVerdict::Yes);
2295        assert_eq!(witness.reason, SelectorMatchReason::SimpleCompound);
2296        assert_eq!(witness.specificity, Specificity::new(1, 3, 1));
2297    }
2298
2299    #[test]
2300    fn reports_missing_class_and_id_as_no_for_exact_signature() {
2301        let element = ElementSignature::concrete(Some("button"), Some("save"), ["primary"]);
2302
2303        let class_miss = selector_match_witness(".missing", &element);
2304        assert_eq!(class_miss.verdict, SelectorMatchVerdict::No);
2305        assert_eq!(class_miss.reason, SelectorMatchReason::MissingClass);
2306        assert!(class_miss.missing_classes.contains("missing"));
2307
2308        let id_miss = selector_match_witness("#cancel", &element);
2309        assert_eq!(id_miss.verdict, SelectorMatchVerdict::No);
2310        assert_eq!(id_miss.reason, SelectorMatchReason::MissingId);
2311        assert_eq!(id_miss.missing_id.as_deref(), Some("cancel"));
2312    }
2313
2314    #[test]
2315    fn returns_maybe_for_inexact_abstract_class_sets() {
2316        let element = ElementSignature::at_least_classes(["button"]);
2317
2318        let witness = selector_match_witness(".button.primary", &element);
2319
2320        assert_eq!(witness.verdict, SelectorMatchVerdict::Maybe);
2321        assert_eq!(witness.reason, SelectorMatchReason::MissingClass);
2322        assert!(witness.missing_classes.contains("primary"));
2323    }
2324
2325    #[test]
2326    fn selector_lists_choose_strongest_matching_branch() {
2327        let element = ElementSignature::concrete(Some("button"), Some("save"), ["primary"]);
2328
2329        let witness = selector_match_witness(".missing, button#save.primary", &element);
2330
2331        assert_eq!(witness.verdict, SelectorMatchVerdict::Yes);
2332        assert_eq!(witness.reason, SelectorMatchReason::SelectorList);
2333        assert_eq!(
2334            witness.matched_branch.as_deref(),
2335            Some("button#save.primary")
2336        );
2337        assert_eq!(witness.specificity, Specificity::new(1, 1, 1));
2338    }
2339
2340    #[test]
2341    fn unsupported_combinators_are_reported_as_maybe() {
2342        let element = ElementSignature::concrete(Some("span"), None::<String>, ["icon"]);
2343
2344        let witness = selector_match_witness(".button > .icon", &element);
2345
2346        assert_eq!(witness.verdict, SelectorMatchVerdict::Maybe);
2347        assert_eq!(witness.reason, SelectorMatchReason::UnsupportedSelector);
2348        assert_eq!(witness.unsupported_branches, vec![".button > .icon"]);
2349    }
2350
2351    #[test]
2352    fn substitutes_custom_property_fallbacks_and_references() {
2353        let mut env = CustomPropertyEnv::new();
2354        env.insert(
2355            "--brand".to_string(),
2356            CascadeValue::Literal("red".to_string()),
2357        );
2358
2359        let resolved = substitute_custom_properties(
2360            &CascadeValue::Var {
2361                name: "--brand".to_string(),
2362                fallback: Some(Box::new(CascadeValue::Literal("blue".to_string()))),
2363            },
2364            &env,
2365        );
2366        assert_eq!(resolved, CascadeValue::Literal("red".to_string()));
2367
2368        let fallback = substitute_custom_properties(
2369            &CascadeValue::Var {
2370                name: "--missing".to_string(),
2371                fallback: Some(Box::new(CascadeValue::Literal("blue".to_string()))),
2372            },
2373            &env,
2374        );
2375        assert_eq!(fallback, CascadeValue::Literal("blue".to_string()));
2376    }
2377
2378    #[test]
2379    fn substitutes_cycles_to_guaranteed_invalid() {
2380        let mut env = CustomPropertyEnv::new();
2381        env.insert(
2382            "--a".to_string(),
2383            CascadeValue::Var {
2384                name: "--b".to_string(),
2385                fallback: None,
2386            },
2387        );
2388        env.insert(
2389            "--b".to_string(),
2390            CascadeValue::Var {
2391                name: "--a".to_string(),
2392                fallback: None,
2393            },
2394        );
2395
2396        let resolved = substitute_custom_properties(
2397            &CascadeValue::Var {
2398                name: "--a".to_string(),
2399                fallback: None,
2400            },
2401            &env,
2402        );
2403
2404        assert_eq!(resolved, CascadeValue::GuaranteedInvalid);
2405    }
2406
2407    #[test]
2408    fn fuzz_seed_corpus_preserves_cascade_and_var_invariants() {
2409        let report = run_cascade_fuzz_seed_corpus();
2410
2411        assert_eq!(report.product, "omena-cascade.fuzz-seed-corpus");
2412        assert_eq!(report.failed_count, 0);
2413        assert_eq!(report.passed_count, report.case_count);
2414        assert!(
2415            report
2416                .var_results
2417                .iter()
2418                .any(|result| result.result == CascadeValue::GuaranteedInvalid)
2419        );
2420    }
2421
2422    #[test]
2423    fn summarizes_current_boundary_status() {
2424        let summary = summarize_cascade_boundary();
2425
2426        assert_eq!(summary.product, "omena-cascade.boundary");
2427        assert_eq!(summary.ordering_model, "lexicographicCascadeKey");
2428        assert!(summary.ready_surfaces.contains(&"cascadeKeyOrdering"));
2429        assert!(summary.ready_surfaces.contains(&"genericCascadeWinner"));
2430        assert!(
2431            summary
2432                .ready_surfaces
2433                .contains(&"semanticDesignTokenRanking")
2434        );
2435        assert!(
2436            summary
2437                .ready_surfaces
2438                .contains(&"queryReadCascadeAtPosition")
2439        );
2440        assert!(summary.ready_surfaces.contains(&"selectorContextWitness"));
2441        assert!(summary.ready_surfaces.contains(&"selectorMatchWitness"));
2442        assert!(
2443            summary
2444                .ready_surfaces
2445                .contains(&"supportsStaticEvalWitness")
2446        );
2447        assert!(summary.ready_surfaces.contains(&"scopeFlattenProof"));
2448        assert!(summary.ready_surfaces.contains(&"layerFlattenProof"));
2449        assert!(summary.ready_surfaces.contains(&"wptCascadeSeedCorpus"));
2450        assert!(
2451            summary
2452                .ready_surfaces
2453                .contains(&"cascadeConformanceSeedCorpus")
2454        );
2455        assert!(!summary.not_ready_surfaces.contains(&"selectorMatchWitness"));
2456        assert!(!summary.not_ready_surfaces.contains(&"wptCascadeCorpus"));
2457        assert!(summary.not_ready_surfaces.contains(&"fullWptCascadeCorpus"));
2458    }
2459
2460    #[test]
2461    fn seed_conformance_corpus_passes_current_cascade_model() {
2462        let report = run_cascade_conformance_seed_corpus();
2463
2464        assert_eq!(report.product, "omena-cascade.conformance-seed-corpus");
2465        assert_eq!(report.case_count, 6);
2466        assert_eq!(report.passed_count, report.case_count);
2467        assert_eq!(report.failed_count, 0);
2468        assert!(report.results.iter().all(|result| result.passed));
2469    }
2470
2471    #[test]
2472    fn wpt_cascade_seed_corpus_passes_current_cascade_model() {
2473        let report = run_wpt_cascade_seed_corpus();
2474
2475        assert_eq!(report.product, "omena-cascade.wpt-cascade-seed-corpus");
2476        assert!(report.case_count >= 200);
2477        assert_eq!(report.passed_count, report.case_count);
2478        assert_eq!(report.failed_count, 0);
2479        assert!(report.results.iter().all(|result| result.passed));
2480    }
2481}