Skip to main content

omena_cascade/
refinement.rs

1//! Refinement entry points layered above the byte-stable cascade proof module.
2
3use omena_refinement_trait::{
4    RefinementVerdictV0, RefinementWitnessV0, refinement_provenance_v0, refinement_witness_v0,
5};
6
7use crate::{
8    CascadeDeclaration, CascadeLevel, LayerFlattenInputV0, ScopeFlattenInputV0,
9    StaticSupportsAssumptionV0, StaticSupportsEvalVerdictV0, evaluate_static_supports_condition,
10    prove_layer_flatten_candidate, prove_scope_flatten_candidate,
11};
12
13#[derive(Debug, Clone, PartialEq, Eq)]
14pub struct CascadeRefinementContextV0 {
15    pub supports_condition: Option<String>,
16    pub scope_root_selector: Option<String>,
17    pub layer_name: Option<String>,
18    pub closed_bundle: bool,
19}
20
21impl Default for CascadeRefinementContextV0 {
22    fn default() -> Self {
23        Self {
24            supports_condition: None,
25            scope_root_selector: None,
26            layer_name: None,
27            closed_bundle: true,
28        }
29    }
30}
31
32pub fn refine_declaration_in_context(
33    declaration: &CascadeDeclaration,
34    context: &CascadeRefinementContextV0,
35) -> RefinementWitnessV0 {
36    let mut provenances = Vec::new();
37    let mut verdicts = Vec::new();
38
39    if let Some(condition) = context.supports_condition.as_deref() {
40        let supports = evaluate_static_supports_condition(
41            condition,
42            StaticSupportsAssumptionV0::ModernBrowser,
43        );
44        provenances.push(refinement_provenance_v0(
45            "supports-predicate",
46            Some("evaluate_static_supports_condition"),
47        ));
48        verdicts.push(match supports.verdict {
49            StaticSupportsEvalVerdictV0::AlwaysTrue => RefinementVerdictV0::SatisfiedAll,
50            StaticSupportsEvalVerdictV0::AlwaysFalse => RefinementVerdictV0::Unsatisfiable,
51            StaticSupportsEvalVerdictV0::Unknown => RefinementVerdictV0::Unknown,
52        });
53    }
54
55    if let Some(root_selector) = context.scope_root_selector.as_deref() {
56        let scope = prove_scope_flatten_candidate(ScopeFlattenInputV0 {
57            root_selector: root_selector.to_string(),
58            limit_selector: None,
59            scoped_rule_count: 1,
60            peer_scope_count: 0,
61            competing_unscoped_rule_count: 0,
62            inside_layer: context.layer_name.is_some(),
63        });
64        provenances.push(refinement_provenance_v0(
65            "scope-predicate",
66            Some("prove_scope_flatten_candidate"),
67        ));
68        verdicts.push(if scope.accepted {
69            RefinementVerdictV0::SatisfiedAll
70        } else {
71            RefinementVerdictV0::Unknown
72        });
73    }
74
75    if context.layer_name.is_some() {
76        let layer = prove_layer_flatten_candidate(LayerFlattenInputV0 {
77            layer_name: context.layer_name.clone(),
78            layer_rule_count: 1,
79            peer_layer_count: 0,
80            unlayered_rule_count: 0,
81            important_declaration_count: usize::from(matches!(
82                declaration.key.level,
83                CascadeLevel::InlineImportant
84                    | CascadeLevel::AuthorImportant
85                    | CascadeLevel::UserImportant
86                    | CascadeLevel::UserAgentImportant
87            )),
88            closed_bundle: context.closed_bundle,
89        });
90        provenances.push(refinement_provenance_v0(
91            "layer-predicate",
92            Some("prove_layer_flatten_candidate"),
93        ));
94        verdicts.push(if layer.accepted {
95            RefinementVerdictV0::SatisfiedAll
96        } else {
97            RefinementVerdictV0::Unknown
98        });
99    }
100
101    let verdict = combine_refinement_verdicts(&verdicts);
102    refinement_witness_v0("cascade-refinement-conjunction", verdict, provenances)
103}
104
105fn combine_refinement_verdicts(verdicts: &[RefinementVerdictV0]) -> RefinementVerdictV0 {
106    if verdicts.is_empty() {
107        return RefinementVerdictV0::SatisfiedAll;
108    }
109    if verdicts.contains(&RefinementVerdictV0::Unsatisfiable) {
110        return RefinementVerdictV0::Unsatisfiable;
111    }
112    if verdicts
113        .iter()
114        .all(|verdict| *verdict == RefinementVerdictV0::SatisfiedAll)
115    {
116        return RefinementVerdictV0::SatisfiedAll;
117    }
118    if verdicts.contains(&RefinementVerdictV0::SatisfiedAll) {
119        RefinementVerdictV0::SatisfiedSome
120    } else {
121        RefinementVerdictV0::Unknown
122    }
123}
124
125#[cfg(test)]
126mod tests {
127    use super::*;
128    use crate::{
129        CascadeKey, CascadeValue, LayerOrdinal, OpenWorldTieEvidence, Specificity,
130        SpecificityExactnessV0, normalized_layer_rank,
131    };
132
133    const EXPECTED_LEGACY_PROOFS_RS_SHA256: [u8; 32] = [
134        0x49, 0xeb, 0x0f, 0x3c, 0x85, 0x80, 0x45, 0x1d, 0x72, 0x9a, 0x57, 0x87, 0x88, 0x0f, 0xc1,
135        0xe1, 0x28, 0x04, 0xda, 0xf0, 0x46, 0x72, 0xd1, 0xcc, 0x4e, 0xd0, 0xab, 0x12, 0xdb, 0x6a,
136        0x7d, 0xb7,
137    ];
138
139    #[test]
140    fn legacy_proofs_rs_byte_untouched() {
141        let digest = sha256(include_bytes!("proofs.rs"));
142        assert_eq!(digest, EXPECTED_LEGACY_PROOFS_RS_SHA256);
143    }
144
145    #[test]
146    fn inline_important_declarations_block_layer_flattening() {
147        let Some(layer_ordinal) = LayerOrdinal::new(0) else {
148            unreachable!("zero must remain a sentinel-safe layer ordinal");
149        };
150        let declaration = CascadeDeclaration {
151            id: "inline-important".to_string(),
152            property: "color".to_string(),
153            value: CascadeValue::Literal("red".to_string()),
154            key: CascadeKey::new(
155                CascadeLevel::InlineImportant,
156                normalized_layer_rank(true, Some(layer_ordinal)),
157                0,
158                Specificity::ZERO,
159                1,
160            ),
161            open_world_tie_evidence: OpenWorldTieEvidence::NONE,
162            specificity_exactness: SpecificityExactnessV0::Exact,
163        };
164        let witness = refine_declaration_in_context(
165            &declaration,
166            &CascadeRefinementContextV0 {
167                layer_name: Some("theme".to_string()),
168                ..CascadeRefinementContextV0::default()
169            },
170        );
171
172        // Omitting InlineImportant from the importance classifier makes this SatisfiedAll.
173        // The production refinement path can emit that false result for this one-layer fixture.
174        assert_eq!(witness.verdict, RefinementVerdictV0::Unknown);
175    }
176
177    fn sha256(input: &[u8]) -> [u8; 32] {
178        const H0: [u32; 8] = [
179            0x6a09e667, 0xbb67ae85, 0x3c6ef372, 0xa54ff53a, 0x510e527f, 0x9b05688c, 0x1f83d9ab,
180            0x5be0cd19,
181        ];
182        const K: [u32; 64] = [
183            0x428a2f98, 0x71374491, 0xb5c0fbcf, 0xe9b5dba5, 0x3956c25b, 0x59f111f1, 0x923f82a4,
184            0xab1c5ed5, 0xd807aa98, 0x12835b01, 0x243185be, 0x550c7dc3, 0x72be5d74, 0x80deb1fe,
185            0x9bdc06a7, 0xc19bf174, 0xe49b69c1, 0xefbe4786, 0x0fc19dc6, 0x240ca1cc, 0x2de92c6f,
186            0x4a7484aa, 0x5cb0a9dc, 0x76f988da, 0x983e5152, 0xa831c66d, 0xb00327c8, 0xbf597fc7,
187            0xc6e00bf3, 0xd5a79147, 0x06ca6351, 0x14292967, 0x27b70a85, 0x2e1b2138, 0x4d2c6dfc,
188            0x53380d13, 0x650a7354, 0x766a0abb, 0x81c2c92e, 0x92722c85, 0xa2bfe8a1, 0xa81a664b,
189            0xc24b8b70, 0xc76c51a3, 0xd192e819, 0xd6990624, 0xf40e3585, 0x106aa070, 0x19a4c116,
190            0x1e376c08, 0x2748774c, 0x34b0bcb5, 0x391c0cb3, 0x4ed8aa4a, 0x5b9cca4f, 0x682e6ff3,
191            0x748f82ee, 0x78a5636f, 0x84c87814, 0x8cc70208, 0x90befffa, 0xa4506ceb, 0xbef9a3f7,
192            0xc67178f2,
193        ];
194
195        let mut bytes = input.to_vec();
196        let bit_len = (bytes.len() as u64) * 8;
197        bytes.push(0x80);
198        while bytes.len() % 64 != 56 {
199            bytes.push(0);
200        }
201        bytes.extend_from_slice(&bit_len.to_be_bytes());
202
203        let mut state = H0;
204        for chunk in bytes.chunks_exact(64) {
205            let mut w = [0u32; 64];
206            for (index, word) in chunk.chunks_exact(4).enumerate() {
207                w[index] = u32::from_be_bytes([word[0], word[1], word[2], word[3]]);
208            }
209            for index in 16..64 {
210                let s0 = w[index - 15].rotate_right(7)
211                    ^ w[index - 15].rotate_right(18)
212                    ^ (w[index - 15] >> 3);
213                let s1 = w[index - 2].rotate_right(17)
214                    ^ w[index - 2].rotate_right(19)
215                    ^ (w[index - 2] >> 10);
216                w[index] = w[index - 16]
217                    .wrapping_add(s0)
218                    .wrapping_add(w[index - 7])
219                    .wrapping_add(s1);
220            }
221
222            let [mut a, mut b, mut c, mut d, mut e, mut f, mut g, mut h] = state;
223            for index in 0..64 {
224                let s1 = e.rotate_right(6) ^ e.rotate_right(11) ^ e.rotate_right(25);
225                let ch = (e & f) ^ ((!e) & g);
226                let temp1 = h
227                    .wrapping_add(s1)
228                    .wrapping_add(ch)
229                    .wrapping_add(K[index])
230                    .wrapping_add(w[index]);
231                let s0 = a.rotate_right(2) ^ a.rotate_right(13) ^ a.rotate_right(22);
232                let maj = (a & b) ^ (a & c) ^ (b & c);
233                let temp2 = s0.wrapping_add(maj);
234                h = g;
235                g = f;
236                f = e;
237                e = d.wrapping_add(temp1);
238                d = c;
239                c = b;
240                b = a;
241                a = temp1.wrapping_add(temp2);
242            }
243
244            for (slot, value) in state.iter_mut().zip([a, b, c, d, e, f, g, h]) {
245                *slot = slot.wrapping_add(value);
246            }
247        }
248
249        let mut digest = [0u8; 32];
250        for (chunk, word) in digest.chunks_exact_mut(4).zip(state) {
251            chunk.copy_from_slice(&word.to_be_bytes());
252        }
253        digest
254    }
255}