Skip to main content

omena_transform_passes/runtime/
observation_projection.rs

1//! Executable observation projections derived from real transform outputs.
2
3use omena_parser::{ParsedSelectorFactKind, StyleDialect, collect_style_facts};
4use omena_transform_cst::{
5    ObservationKindV0, TransformObservationEquivalenceV0, TransformPassKind,
6    compare_raw_transform_observation_bytes_v0, compare_transform_observation_projection_values_v0,
7};
8use serde::Serialize;
9
10use crate::{
11    TransformWinnerEqualityObservationV0, execute_transform_passes_on_source,
12    runtime::winner_equality::compare_transform_winner_equality_for_conformance_v0,
13};
14
15#[derive(Debug, Clone, PartialEq, Eq, Serialize)]
16#[serde(rename_all = "camelCase")]
17pub struct TransformExecutableObservationProjectionV0 {
18    pub kind: ObservationKindV0,
19    pub value: String,
20}
21
22#[derive(Debug, Clone, PartialEq, Eq, Serialize)]
23#[serde(rename_all = "camelCase")]
24pub struct TransformExecutableObservationReportV0 {
25    pub schema_version: &'static str,
26    pub product: &'static str,
27    pub pass_id: &'static str,
28    pub pass_executed: bool,
29    pub input_projection: TransformExecutableObservationProjectionV0,
30    pub output_projection: TransformExecutableObservationProjectionV0,
31    pub projection_equivalence: TransformObservationEquivalenceV0,
32    pub raw_equivalence: TransformObservationEquivalenceV0,
33    pub output_css: String,
34}
35
36#[derive(Debug, Clone, PartialEq, Eq, Serialize)]
37#[serde(rename_all = "camelCase")]
38pub struct TransformExecutableCascadeWinnerEquivalenceV0 {
39    pub schema_version: &'static str,
40    pub product: &'static str,
41    pub profile_id: &'static str,
42    pub compared_obligation_count: usize,
43    pub equivalent: bool,
44}
45
46/// Execute one product pass and project selector matching from parser-owned
47/// selector facts on both the input and the actual pass output.
48pub fn execute_transform_pass_selector_matching_observation_v0(
49    source: &str,
50    pass: TransformPassKind,
51) -> TransformExecutableObservationReportV0 {
52    let execution = execute_transform_passes_on_source(source, &[pass]);
53    let input_projection = selector_matching_projection_v0(source);
54    let output_projection = selector_matching_projection_v0(execution.output_css.as_str());
55    TransformExecutableObservationReportV0 {
56        schema_version: "0",
57        product: "omena-transform-passes.executable-observation-projection",
58        pass_id: pass.id(),
59        pass_executed: execution.executed_pass_ids.contains(&pass.id()),
60        projection_equivalence: compare_transform_observation_projection_values_v0(
61            "selector-matching-executable-v0",
62            ObservationKindV0::SelectorMatching,
63            input_projection.as_str(),
64            output_projection.as_str(),
65        ),
66        raw_equivalence: compare_raw_transform_observation_bytes_v0(
67            "raw-bytes-executable-v0",
68            source.as_bytes(),
69            execution.output_css.as_bytes(),
70        ),
71        input_projection: TransformExecutableObservationProjectionV0 {
72            kind: ObservationKindV0::SelectorMatching,
73            value: input_projection,
74        },
75        output_projection: TransformExecutableObservationProjectionV0 {
76            kind: ObservationKindV0::SelectorMatching,
77            value: output_projection,
78        },
79        output_css: execution.output_css,
80    }
81}
82
83/// Compare cascade winners using the cascade authority's executable
84/// conformance projection. Empty or absent observations are conservative RED.
85pub fn compare_transform_cascade_winner_observation_v0(
86    input: &str,
87    output: &str,
88    pass: TransformPassKind,
89) -> TransformExecutableCascadeWinnerEquivalenceV0 {
90    let obligations = compare_transform_winner_equality_for_conformance_v0(
91        input,
92        output,
93        StyleDialect::Css,
94        pass,
95    );
96    let equivalent = !obligations.is_empty()
97        && obligations.iter().all(|obligation| {
98            matches!(
99                obligation.observation,
100                TransformWinnerEqualityObservationV0::ObservedEqual { .. }
101                    | TransformWinnerEqualityObservationV0::ObservedGuardedEqual { .. }
102            )
103        });
104    TransformExecutableCascadeWinnerEquivalenceV0 {
105        schema_version: "0",
106        product: "omena-transform-passes.executable-cascade-winner-equivalence",
107        profile_id: "cascade-winner-executable-v0",
108        compared_obligation_count: obligations.len(),
109        equivalent,
110    }
111}
112
113fn selector_matching_projection_v0(source: &str) -> String {
114    let mut selectors = collect_style_facts(source, StyleDialect::Css)
115        .selectors
116        .into_iter()
117        .map(|selector| {
118            let kind = match selector.kind {
119                ParsedSelectorFactKind::Class => "class",
120                ParsedSelectorFactKind::Id => "id",
121                ParsedSelectorFactKind::Placeholder => "placeholder",
122            };
123            format!("{kind}:{}", selector.name)
124        })
125        .collect::<Vec<_>>();
126    selectors.sort();
127    selectors.dedup();
128    selectors.join("\n")
129}
130
131#[cfg(test)]
132mod tests {
133    use serde::Deserialize;
134
135    use super::*;
136
137    #[derive(Deserialize)]
138    #[serde(rename_all = "camelCase")]
139    struct ExecutableTruthRowV0 {
140        case_id: String,
141        pass_id: String,
142        observation_kind: String,
143        input_css: String,
144        candidate_output_css: Option<String>,
145        expected_equivalent: bool,
146    }
147
148    #[test]
149    fn executable_truth_table_runs_product_pass_and_authority_projections()
150    -> Result<(), Box<dyn std::error::Error>> {
151        let rows = serde_json::from_str::<Vec<ExecutableTruthRowV0>>(include_str!(
152            "../../data/observation-executable-truth-table-v0.json"
153        ))?;
154        let mut positive_rows = 0usize;
155        for row in rows {
156            if row.pass_id != TransformPassKind::WhitespaceStrip.id() {
157                return Err(format!("unknown executable pass in {}", row.case_id).into());
158            }
159            let report = execute_transform_pass_selector_matching_observation_v0(
160                row.input_css.as_str(),
161                TransformPassKind::WhitespaceStrip,
162            );
163            assert!(report.pass_executed, "{}", row.case_id);
164            let equivalent = match row.observation_kind.as_str() {
165                "selectorMatching" => report.projection_equivalence.equivalent,
166                "rawBytes" => report.raw_equivalence.equivalent,
167                "cascadeWinner" => {
168                    compare_transform_cascade_winner_observation_v0(
169                        row.input_css.as_str(),
170                        row.candidate_output_css
171                            .as_deref()
172                            .unwrap_or(report.output_css.as_str()),
173                        TransformPassKind::WhitespaceStrip,
174                    )
175                    .equivalent
176                }
177                other => return Err(format!("unknown executable observer: {other}").into()),
178            };
179            assert_eq!(equivalent, row.expected_equivalent, "{}", row.case_id);
180            positive_rows += usize::from(equivalent);
181        }
182        assert!(
183            positive_rows > 0,
184            "executable truth table has no positive row"
185        );
186        Ok(())
187    }
188
189    #[test]
190    fn executed_whitespace_pass_is_selector_equivalent_but_not_raw_equivalent() {
191        let report = execute_transform_pass_selector_matching_observation_v0(
192            ".a { color: red; }",
193            TransformPassKind::WhitespaceStrip,
194        );
195        println!(
196            "selectorProjection passExecuted={} pass={} selectorMatching={} rawBytes={} output={:?}",
197            report.pass_executed,
198            report.pass_id,
199            report.projection_equivalence.equivalent,
200            report.raw_equivalence.equivalent,
201            report.output_css,
202        );
203        assert!(report.pass_executed);
204        assert!(report.projection_equivalence.equivalent);
205        assert!(!report.raw_equivalence.equivalent);
206    }
207
208    #[test]
209    fn executed_pass_baseline_detects_cascade_winner_corruption() {
210        let source = ".a { color: red; } .a { color: blue; }";
211        let report = execute_transform_pass_selector_matching_observation_v0(
212            source,
213            TransformPassKind::WhitespaceStrip,
214        );
215        let pass_relation = compare_transform_cascade_winner_observation_v0(
216            source,
217            report.output_css.as_str(),
218            TransformPassKind::WhitespaceStrip,
219        );
220        let corrupted = ".a{color:blue}.a{color:red}";
221        let corrupted_relation = compare_transform_cascade_winner_observation_v0(
222            source,
223            corrupted,
224            TransformPassKind::WhitespaceStrip,
225        );
226        println!(
227            "cascadeWinnerProjection passExecuted={} pass={} passCascadeWinner={} corruptedCascadeWinner={} disjointSelectorMatching={} passOutput={:?}",
228            report.pass_executed,
229            report.pass_id,
230            pass_relation.equivalent,
231            corrupted_relation.equivalent,
232            report.projection_equivalence.equivalent,
233            report.output_css,
234        );
235        assert!(report.pass_executed);
236        assert!(pass_relation.equivalent);
237        assert!(!corrupted_relation.equivalent);
238        assert!(report.projection_equivalence.equivalent);
239    }
240}