Skip to main content

omena_cascade_proof/
proof_kernel.rs

1//! Small, solver-free rewrite certificate checker.
2//!
3//! The checker derives both endpoints from a certificate and compares those
4//! derived terms with the supplied endpoints. It never calls a rewrite search,
5//! reads a transform proof object, or accepts a producer-owned boolean.
6//!
7//! This kernel has deliberately narrow authority:
8//! - it cannot establish that a rule catalog is sound;
9//! - it cannot establish that an observer profile models browser behaviour;
10//! - it cannot detect a defect shared with an external side-condition source;
11//! - it cannot see a defect that moves both sides of a comparison identically;
12//! - for genuinely IR-computed requirements, it can add disclosure without
13//!   adding independent semantic strength.
14
15use std::collections::{BTreeMap, BTreeSet};
16
17use omena_cascade::{
18    CascadeKey, CascadeLevel, CascadeValue, DomClassTokenizationV0, LayerOrdinal, Specificity,
19    normalized_layer_rank, resolve_custom_property_env_least_fixed_point, token_support_v0,
20    tokenize_dom_class_attribute_v0,
21};
22use serde::{Deserialize, Serialize};
23
24pub const REWRITE_CERTIFICATE_SCHEMA_VERSION_V0: &str = "0";
25pub const REWRITE_RULE_CATALOG_SCHEMA_VERSION_V0: &str = "0";
26pub const REWRITE_RULE_CATALOG_SCHEMA_ID_V0: &str = "omena-cascade-proof.rewrite-rule-catalog.v0";
27pub const CANONICAL_REWRITE_ASSUMPTIONS_SCHEMA_VERSION_V0: &str = "0";
28pub const REWRITE_CERTIFICATE_MAX_DEPTH_V0: usize = 64;
29pub const REWRITE_CERTIFICATE_MAX_NODES_V0: usize = 4_096;
30pub const REWRITE_RULE_CATALOG_MAX_RULES_V0: usize = 256;
31pub const REWRITE_RULE_CATALOG_MAX_OPERATORS_V0: usize = 256;
32const REWRITE_TERM_MAX_DEPTH_V0: usize = 64;
33const REWRITE_TERM_MAX_NODES_V0: usize = 4_096;
34
35#[derive(Debug, Clone, PartialEq, Eq, Hash, Deserialize, Serialize)]
36#[non_exhaustive]
37#[serde(
38    tag = "kind",
39    rename_all = "camelCase",
40    rename_all_fields = "camelCase"
41)]
42pub enum RewriteTermV0 {
43    Atom {
44        value: String,
45    },
46    Apply {
47        operator: String,
48        operands: Vec<RewriteTermV0>,
49    },
50}
51
52impl RewriteTermV0 {
53    pub fn atom(value: impl Into<String>) -> Self {
54        Self::Atom {
55            value: value.into(),
56        }
57    }
58
59    pub fn apply(operator: impl Into<String>, operands: Vec<Self>) -> Self {
60        Self::Apply {
61            operator: operator.into(),
62            operands,
63        }
64    }
65}
66
67#[derive(Debug, Clone, PartialEq, Eq, Deserialize, Serialize)]
68#[non_exhaustive]
69#[serde(
70    tag = "kind",
71    rename_all = "camelCase",
72    rename_all_fields = "camelCase"
73)]
74pub enum RewritePatternV0 {
75    Atom {
76        value: String,
77    },
78    Variable {
79        name: String,
80    },
81    Apply {
82        operator: String,
83        operands: Vec<RewritePatternV0>,
84    },
85}
86
87impl RewritePatternV0 {
88    pub fn atom(value: impl Into<String>) -> Self {
89        Self::Atom {
90            value: value.into(),
91        }
92    }
93
94    pub fn variable(name: impl Into<String>) -> Self {
95        Self::Variable { name: name.into() }
96    }
97
98    pub fn apply(operator: impl Into<String>, operands: Vec<Self>) -> Self {
99        Self::Apply {
100            operator: operator.into(),
101            operands,
102        }
103    }
104}
105
106#[derive(Debug, Clone, Copy, PartialEq, Eq, Deserialize, Serialize)]
107#[non_exhaustive]
108#[serde(rename_all = "camelCase")]
109pub enum CascadeLevelCertV0 {
110    UserAgentNormal,
111    UserNormal,
112    AuthorNormal,
113    InlineNormal,
114    Animation,
115    AuthorImportant,
116    InlineImportant,
117    UserImportant,
118    UserAgentImportant,
119    Transition,
120}
121
122impl CascadeLevelCertV0 {
123    fn to_cascade_level(self) -> CascadeLevel {
124        match self {
125            Self::UserAgentNormal => CascadeLevel::UserAgentNormal,
126            Self::UserNormal => CascadeLevel::UserNormal,
127            Self::AuthorNormal => CascadeLevel::AuthorNormal,
128            Self::InlineNormal => CascadeLevel::InlineNormal,
129            Self::Animation => CascadeLevel::Animation,
130            Self::AuthorImportant => CascadeLevel::AuthorImportant,
131            Self::InlineImportant => CascadeLevel::InlineImportant,
132            Self::UserImportant => CascadeLevel::UserImportant,
133            Self::UserAgentImportant => CascadeLevel::UserAgentImportant,
134            Self::Transition => CascadeLevel::Transition,
135        }
136    }
137}
138
139#[derive(Debug, Clone, PartialEq, Eq, Deserialize, Serialize)]
140#[serde(rename_all = "camelCase")]
141pub struct CascadeWinnerKeyCertV0 {
142    pub level: CascadeLevelCertV0,
143    pub layer_important: bool,
144    pub layer_ordinal: Option<i32>,
145    pub scope_proximity: u32,
146    pub specificity_ids: u32,
147    pub specificity_classes: u32,
148    pub specificity_elements: u32,
149    pub source_order: u32,
150}
151
152#[derive(Debug, Clone, PartialEq, Eq, Deserialize, Serialize)]
153#[serde(rename_all = "camelCase")]
154pub struct CascadeWinnerEqualityCertV0 {
155    pub before_winner_id: String,
156    pub after_winner_id: String,
157    pub before_key: CascadeWinnerKeyCertV0,
158    pub after_key: CascadeWinnerKeyCertV0,
159    pub before_class_attribute: String,
160    pub after_class_attribute: String,
161}
162
163#[derive(Debug, Clone, PartialEq, Eq, Deserialize, Serialize)]
164#[non_exhaustive]
165#[serde(
166    tag = "kind",
167    rename_all = "camelCase",
168    rename_all_fields = "camelCase"
169)]
170pub enum ComputedValueTermV0 {
171    Literal {
172        value: String,
173    },
174    Composite {
175        values: Vec<ComputedValueTermV0>,
176    },
177    Variable {
178        name: String,
179        fallback: Option<Box<ComputedValueTermV0>>,
180    },
181    Initial,
182    Inherit,
183    Indeterminate,
184    GuaranteedInvalid,
185    Unset,
186}
187
188impl ComputedValueTermV0 {
189    fn to_cascade_value(&self) -> CascadeValue {
190        match self {
191            Self::Literal { value } => CascadeValue::Literal(value.clone()),
192            Self::Composite { values } => CascadeValue::Composite(
193                values
194                    .iter()
195                    .map(ComputedValueTermV0::to_cascade_value)
196                    .collect(),
197            ),
198            Self::Variable { name, fallback } => CascadeValue::Var {
199                name: name.clone(),
200                fallback: fallback
201                    .as_ref()
202                    .map(|value| Box::new(value.to_cascade_value())),
203            },
204            Self::Initial => CascadeValue::Initial,
205            Self::Inherit => CascadeValue::Inherit,
206            Self::Indeterminate => CascadeValue::Indeterminate,
207            Self::GuaranteedInvalid => CascadeValue::GuaranteedInvalid,
208            Self::Unset => CascadeValue::Unset,
209        }
210    }
211}
212
213#[derive(Debug, Clone, PartialEq, Eq, Deserialize, Serialize)]
214#[serde(rename_all = "camelCase")]
215pub struct ComputedValueEnvironmentEntryV0 {
216    pub name: String,
217    pub value: ComputedValueTermV0,
218}
219
220#[derive(Debug, Clone, PartialEq, Eq, Deserialize, Serialize)]
221#[serde(rename_all = "camelCase")]
222pub struct ComputedValueEqualityCertV0 {
223    pub property: String,
224    pub before_environment: Vec<ComputedValueEnvironmentEntryV0>,
225    pub after_environment: Vec<ComputedValueEnvironmentEntryV0>,
226}
227
228#[derive(Debug, Clone, PartialEq, Eq, Deserialize, Serialize)]
229#[serde(rename_all = "camelCase")]
230pub struct SourceMapTraceSegmentV0 {
231    pub source_path: String,
232    pub source_digest: String,
233    pub original_start: usize,
234    pub original_end: usize,
235    pub generated_start: usize,
236    pub generated_end: usize,
237    pub pass_id: String,
238}
239
240#[derive(Debug, Clone, PartialEq, Eq, Deserialize, Serialize)]
241#[serde(rename_all = "camelCase")]
242pub struct SourceMapTraceCertV0 {
243    pub before_segments: Vec<SourceMapTraceSegmentV0>,
244    pub after_segments: Vec<SourceMapTraceSegmentV0>,
245}
246
247#[derive(Debug, Clone, PartialEq, Eq, Deserialize, Serialize)]
248#[serde(rename_all = "camelCase")]
249pub struct TokenOwnershipCertEntryV0 {
250    pub emitted_token: String,
251    pub module_paths: Vec<String>,
252}
253
254#[derive(Debug, Clone, PartialEq, Eq, Deserialize, Serialize)]
255#[serde(rename_all = "camelCase")]
256pub struct TokenOwnershipSeparabilityCertV0 {
257    pub complete: bool,
258    pub modeled_preimage_count: usize,
259    pub emitted_token_count: usize,
260    pub ownerships: Vec<TokenOwnershipCertEntryV0>,
261    pub unattributed_emitted_token_count: usize,
262    pub interface_mismatch_count: usize,
263}
264
265#[derive(Debug, Clone, PartialEq, Eq, Deserialize, Serialize)]
266#[serde(rename_all = "camelCase")]
267pub struct TransformIndependenceObservationCertRowV0 {
268    pub fixture_id: String,
269    pub observer: String,
270    pub left_then_right: String,
271    pub right_then_left: String,
272}
273
274#[derive(Debug, Clone, PartialEq, Eq, Deserialize, Serialize)]
275#[serde(rename_all = "camelCase")]
276pub struct TransformIndependenceCertV0 {
277    pub left_pass_id: String,
278    pub right_pass_id: String,
279    pub observation_profile_id: String,
280    pub profile_observers: Vec<String>,
281    pub observation_rows: Vec<TransformIndependenceObservationCertRowV0>,
282    pub left_preconditions: Vec<String>,
283    pub right_preconditions: Vec<String>,
284    pub left_preserves_right_preconditions: Vec<String>,
285    pub right_preserves_left_preconditions: Vec<String>,
286    pub disqualifying_descriptor_edges: Vec<String>,
287}
288
289#[derive(Debug, Clone, Copy, PartialEq, Eq, Deserialize, Serialize)]
290#[non_exhaustive]
291#[serde(rename_all = "camelCase")]
292pub enum RewriteSideConditionKindV0 {
293    NoSideCondition,
294    CascadeWinnerEquality,
295    ComputedValueEquality,
296    SourceMapTrace,
297    TokenOwnershipSeparability,
298    TransformIndependence,
299}
300
301#[derive(Debug, Clone, PartialEq, Eq, Deserialize, Serialize)]
302#[non_exhaustive]
303#[serde(tag = "kind", rename_all = "camelCase")]
304pub enum SideConditionCertV0 {
305    NoSideCondition,
306    CascadeWinnerEquality {
307        certificate: CascadeWinnerEqualityCertV0,
308    },
309    ComputedValueEquality {
310        certificate: ComputedValueEqualityCertV0,
311    },
312    SourceMapTrace {
313        certificate: SourceMapTraceCertV0,
314    },
315    TokenOwnershipSeparability {
316        certificate: TokenOwnershipSeparabilityCertV0,
317    },
318    TransformIndependence {
319        certificate: Box<TransformIndependenceCertV0>,
320    },
321}
322
323impl SideConditionCertV0 {
324    fn kind(&self) -> RewriteSideConditionKindV0 {
325        match self {
326            Self::NoSideCondition => RewriteSideConditionKindV0::NoSideCondition,
327            Self::CascadeWinnerEquality { .. } => RewriteSideConditionKindV0::CascadeWinnerEquality,
328            Self::ComputedValueEquality { .. } => RewriteSideConditionKindV0::ComputedValueEquality,
329            Self::SourceMapTrace { .. } => RewriteSideConditionKindV0::SourceMapTrace,
330            Self::TokenOwnershipSeparability { .. } => {
331                RewriteSideConditionKindV0::TokenOwnershipSeparability
332            }
333            Self::TransformIndependence { .. } => RewriteSideConditionKindV0::TransformIndependence,
334        }
335    }
336}
337
338#[derive(Debug, Clone, PartialEq, Eq, Deserialize, Serialize)]
339#[serde(rename_all = "camelCase")]
340pub struct RewriteOperatorV0 {
341    pub operator: String,
342    pub arity: usize,
343}
344
345#[derive(Debug, Clone, PartialEq, Eq, Deserialize, Serialize)]
346#[serde(rename_all = "camelCase")]
347pub struct RewriteRuleV0 {
348    pub rule_id: String,
349    pub before_pattern: RewritePatternV0,
350    pub after_pattern: RewritePatternV0,
351    pub side_condition_kind: RewriteSideConditionKindV0,
352}
353
354#[derive(Debug, Clone, PartialEq, Eq, Deserialize, Serialize)]
355#[serde(rename_all = "camelCase")]
356pub struct RewriteRuleCatalogV0 {
357    pub schema_version: String,
358    pub operators: Vec<RewriteOperatorV0>,
359    pub rules: Vec<RewriteRuleV0>,
360}
361
362#[derive(Debug, Clone, PartialEq, Eq, Deserialize, Serialize)]
363#[serde(rename_all = "camelCase")]
364pub struct RewriteSubstitutionEntryV0 {
365    pub variable: String,
366    pub term: RewriteTermV0,
367}
368
369#[derive(Debug, Clone, PartialEq, Eq, Deserialize, Serialize)]
370#[non_exhaustive]
371#[serde(
372    tag = "kind",
373    rename_all = "camelCase",
374    rename_all_fields = "camelCase"
375)]
376pub enum RewriteCertificateV0 {
377    Refl {
378        term: RewriteTermV0,
379    },
380    Sym {
381        certificate: Box<RewriteCertificateV0>,
382    },
383    Trans {
384        left: Box<RewriteCertificateV0>,
385        right: Box<RewriteCertificateV0>,
386    },
387    Cong {
388        operator: String,
389        certificates: Vec<RewriteCertificateV0>,
390    },
391    Rewrite {
392        rule_id: String,
393        substitution: Vec<RewriteSubstitutionEntryV0>,
394        side_condition: SideConditionCertV0,
395    },
396}
397
398#[derive(Debug, Clone, PartialEq, Eq, Deserialize, Serialize)]
399#[serde(rename_all = "camelCase")]
400pub struct RewriteCertificateEnvelopeV0 {
401    pub schema_version: String,
402    pub max_depth: usize,
403    pub max_nodes: usize,
404    pub certificate: RewriteCertificateV0,
405}
406
407#[derive(Debug, Clone, PartialEq, Eq, Deserialize, Serialize)]
408#[serde(rename_all = "camelCase")]
409pub struct CanonicalRewriteAssumptionV0 {
410    pub name: String,
411    pub value: String,
412}
413
414#[derive(Debug, Clone, PartialEq, Eq, Deserialize, Serialize)]
415#[serde(rename_all = "camelCase")]
416pub struct CanonicalRewriteAssumptionsV0 {
417    pub schema_version: String,
418    pub entries: Vec<CanonicalRewriteAssumptionV0>,
419}
420
421impl Default for CanonicalRewriteAssumptionsV0 {
422    fn default() -> Self {
423        Self {
424            schema_version: CANONICAL_REWRITE_ASSUMPTIONS_SCHEMA_VERSION_V0.to_owned(),
425            entries: Vec::new(),
426        }
427    }
428}
429
430#[derive(Debug, Clone, Copy, PartialEq, Eq, Deserialize, Serialize)]
431#[non_exhaustive]
432#[serde(rename_all = "camelCase")]
433pub enum RewriteCheckInputV0 {
434    BeforeTerm,
435    AfterTerm,
436    RuleCatalog,
437    Certificate,
438    Assumptions,
439    SerializedCertificate,
440}
441
442#[derive(Debug, Clone, PartialEq, Eq, Deserialize, Serialize)]
443#[serde(rename_all = "camelCase")]
444pub struct RewriteFailureSiteV0 {
445    pub input: RewriteCheckInputV0,
446    pub certificate_path: Vec<usize>,
447    pub term_path: Vec<usize>,
448    pub rule_id: Option<String>,
449}
450
451impl RewriteFailureSiteV0 {
452    fn root(input: RewriteCheckInputV0) -> Self {
453        Self {
454            input,
455            certificate_path: Vec::new(),
456            term_path: Vec::new(),
457            rule_id: None,
458        }
459    }
460
461    fn certificate(certificate_path: &[usize]) -> Self {
462        Self {
463            input: RewriteCheckInputV0::Certificate,
464            certificate_path: certificate_path.to_vec(),
465            term_path: Vec::new(),
466            rule_id: None,
467        }
468    }
469
470    fn rule(rule_id: &str, term_path: Vec<usize>) -> Self {
471        Self {
472            input: RewriteCheckInputV0::RuleCatalog,
473            certificate_path: Vec::new(),
474            term_path,
475            rule_id: Some(rule_id.to_owned()),
476        }
477    }
478}
479
480#[derive(Debug, Clone, PartialEq, Eq, Deserialize, Serialize)]
481#[non_exhaustive]
482#[serde(
483    tag = "kind",
484    rename_all = "camelCase",
485    rename_all_fields = "camelCase"
486)]
487pub enum CertificateRejectionKindV0 {
488    SchemaVersionMismatch {
489        expected: String,
490        observed: String,
491    },
492    BoundOutOfRange {
493        bound: String,
494        declared: usize,
495        hard_maximum: usize,
496    },
497    DeclaredBoundExceeded {
498        bound: String,
499        declared: usize,
500        observed: usize,
501    },
502    CatalogLimitExceeded {
503        collection: String,
504        observed: usize,
505        maximum: usize,
506    },
507    DuplicateOperator {
508        operator: String,
509    },
510    DuplicateRule {
511        rule_id: String,
512    },
513    UnknownOperator {
514        operator: String,
515    },
516    OperatorArityMismatch {
517        operator: String,
518        expected: usize,
519        observed: usize,
520    },
521    EmptyVariable,
522    DuplicateAssumption {
523        name: String,
524    },
525    DuplicateSubstitutionVariable {
526        variable: String,
527    },
528    MissingSubstitutionVariable {
529        variable: String,
530    },
531    UnexpectedSubstitutionVariable {
532        variable: String,
533    },
534    UnknownRule {
535        rule_id: String,
536    },
537    SideConditionKindMismatch {
538        expected: RewriteSideConditionKindV0,
539        observed: RewriteSideConditionKindV0,
540    },
541    InvalidLayerOrdinal {
542        observed: i32,
543    },
544    CascadeWinnerEqualityRejected {
545        winner_ids_equal: bool,
546        cascade_keys_equal: bool,
547        token_support_equal: bool,
548    },
549    CascadeTokenizationUnavailable,
550    DuplicateComputedValueEnvironmentEntry {
551        name: String,
552    },
553    ComputedValueEqualityRejected {
554        property: String,
555        before_present: bool,
556        after_present: bool,
557    },
558    EmptySourceMapTrace,
559    SourceMapTraceRejected {
560        segment_index: usize,
561        reason: String,
562    },
563    TokenOwnershipSeparabilityRejected {
564        reason: String,
565        token: Option<String>,
566    },
567    TransformIndependenceRejected {
568        reason: String,
569        left_pass_id: String,
570        right_pass_id: String,
571    },
572    TransitiveMiddleMismatch,
573    EndpointMismatch {
574        endpoint: RewriteCheckInputV0,
575    },
576    MissingCertificate,
577    MalformedCertificate {
578        message: String,
579    },
580    DerivedTermLimitExceeded,
581}
582
583#[derive(Debug, Clone, PartialEq, Eq, Deserialize, Serialize)]
584#[serde(rename_all = "camelCase")]
585pub struct CertificateRejectionV0 {
586    pub site: Box<RewriteFailureSiteV0>,
587    pub rejection: Box<CertificateRejectionKindV0>,
588}
589
590impl CertificateRejectionV0 {
591    fn new(site: RewriteFailureSiteV0, rejection: CertificateRejectionKindV0) -> Self {
592        Self {
593            site: Box::new(site),
594            rejection: Box::new(rejection),
595        }
596    }
597}
598
599#[derive(Debug, Clone, Copy, PartialEq, Eq)]
600struct RewriteIssuanceSealV0(());
601
602/// Token issued only after the checker derives and matches both endpoints.
603///
604/// The fields are private and the type has no public constructor.
605///
606/// ```compile_fail
607/// use omena_cascade_proof::RewriteIssuanceTokenV0;
608///
609/// let _token = RewriteIssuanceTokenV0 {
610///     before_digest: [0; 32],
611///     after_digest: [0; 32],
612///     catalog_schema_id: "caller-owned",
613///     catalog_content_digest: [0; 32],
614///     checked_rule_ids: Vec::new(),
615///     _seal: loop {},
616/// };
617/// ```
618#[derive(Debug, Clone, PartialEq, Eq)]
619pub struct RewriteIssuanceTokenV0 {
620    before_digest: [u8; 32],
621    after_digest: [u8; 32],
622    catalog_schema_id: &'static str,
623    catalog_content_digest: [u8; 32],
624    checked_rule_ids: Vec<String>,
625    _seal: RewriteIssuanceSealV0,
626}
627
628impl RewriteIssuanceTokenV0 {
629    fn issue(
630        before: &RewriteTermV0,
631        after: &RewriteTermV0,
632        catalog: &RewriteRuleCatalogV0,
633        checked_rule_ids: Vec<String>,
634    ) -> Self {
635        Self {
636            before_digest: term_digest_v0(before),
637            after_digest: term_digest_v0(after),
638            catalog_schema_id: REWRITE_RULE_CATALOG_SCHEMA_ID_V0,
639            catalog_content_digest: catalog_content_digest_v0(catalog),
640            checked_rule_ids,
641            _seal: RewriteIssuanceSealV0(()),
642        }
643    }
644
645    pub fn before_digest_hex_v0(&self) -> String {
646        digest_hex_v0(&self.before_digest)
647    }
648
649    pub fn after_digest_hex_v0(&self) -> String {
650        digest_hex_v0(&self.after_digest)
651    }
652
653    pub fn checked_rule_ids_v0(&self) -> &[String] {
654        self.checked_rule_ids.as_slice()
655    }
656
657    pub const fn catalog_schema_id_v0(&self) -> &'static str {
658        self.catalog_schema_id
659    }
660
661    pub fn catalog_content_digest_hex_v0(&self) -> String {
662        digest_hex_v0(&self.catalog_content_digest)
663    }
664
665    /// Re-bind a sealed issuance token to the exact endpoints a consumer is
666    /// about to apply. A token for a different rewrite pair is not reusable.
667    pub fn matches_endpoints_v0(&self, before: &RewriteTermV0, after: &RewriteTermV0) -> bool {
668        self.before_digest == term_digest_v0(before) && self.after_digest == term_digest_v0(after)
669    }
670
671    /// Compare the sealed catalog identity with catalog content selected by a
672    /// consumer. Callers do not supply a digest: it is re-derived here from
673    /// the catalog value the consumer trusts.
674    pub fn matches_catalog_v0(&self, catalog: &RewriteRuleCatalogV0) -> bool {
675        self.catalog_schema_id == REWRITE_RULE_CATALOG_SCHEMA_ID_V0
676            && self.catalog_content_digest == catalog_content_digest_v0(catalog)
677    }
678}
679
680struct ValidatedCatalogV0<'a> {
681    operators: BTreeMap<&'a str, usize>,
682    rules: BTreeMap<&'a str, &'a RewriteRuleV0>,
683}
684
685struct DerivedRewriteV0 {
686    before: RewriteTermV0,
687    after: RewriteTermV0,
688    checked_rule_ids: Vec<String>,
689}
690
691pub fn check_rewrite_certificate_v0(
692    before: &RewriteTermV0,
693    after: &RewriteTermV0,
694    rule_catalog: &RewriteRuleCatalogV0,
695    certificate: &RewriteCertificateEnvelopeV0,
696    assumptions: &CanonicalRewriteAssumptionsV0,
697) -> Result<RewriteIssuanceTokenV0, CertificateRejectionV0> {
698    validate_schema(
699        &certificate.schema_version,
700        REWRITE_CERTIFICATE_SCHEMA_VERSION_V0,
701        RewriteCheckInputV0::Certificate,
702    )?;
703    validate_certificate_bounds(certificate)?;
704    let catalog = validate_catalog(rule_catalog)?;
705    validate_assumptions(assumptions)?;
706    validate_term(
707        before,
708        RewriteCheckInputV0::BeforeTerm,
709        &[],
710        &catalog.operators,
711    )?;
712    validate_term(
713        after,
714        RewriteCheckInputV0::AfterTerm,
715        &[],
716        &catalog.operators,
717    )?;
718
719    let mut path = Vec::new();
720    let derived = derive_certificate(&certificate.certificate, &catalog, &mut path)?;
721    if derived.before != *before {
722        let term_path = first_term_mismatch_path(before, &derived.before);
723        return Err(CertificateRejectionV0::new(
724            RewriteFailureSiteV0 {
725                input: RewriteCheckInputV0::BeforeTerm,
726                certificate_path: Vec::new(),
727                term_path,
728                rule_id: None,
729            },
730            CertificateRejectionKindV0::EndpointMismatch {
731                endpoint: RewriteCheckInputV0::BeforeTerm,
732            },
733        ));
734    }
735    if derived.after != *after {
736        let term_path = first_term_mismatch_path(after, &derived.after);
737        return Err(CertificateRejectionV0::new(
738            RewriteFailureSiteV0 {
739                input: RewriteCheckInputV0::AfterTerm,
740                certificate_path: Vec::new(),
741                term_path,
742                rule_id: None,
743            },
744            CertificateRejectionKindV0::EndpointMismatch {
745                endpoint: RewriteCheckInputV0::AfterTerm,
746            },
747        ));
748    }
749
750    Ok(RewriteIssuanceTokenV0::issue(
751        before,
752        after,
753        rule_catalog,
754        derived.checked_rule_ids,
755    ))
756}
757
758pub fn check_optional_rewrite_certificate_v0(
759    before: &RewriteTermV0,
760    after: &RewriteTermV0,
761    rule_catalog: &RewriteRuleCatalogV0,
762    certificate: Option<&RewriteCertificateEnvelopeV0>,
763    assumptions: &CanonicalRewriteAssumptionsV0,
764) -> Result<RewriteIssuanceTokenV0, CertificateRejectionV0> {
765    let Some(certificate) = certificate else {
766        return Err(CertificateRejectionV0::new(
767            RewriteFailureSiteV0::root(RewriteCheckInputV0::Certificate),
768            CertificateRejectionKindV0::MissingCertificate,
769        ));
770    };
771    check_rewrite_certificate_v0(before, after, rule_catalog, certificate, assumptions)
772}
773
774pub fn check_serialized_rewrite_certificate_v0(
775    before: &RewriteTermV0,
776    after: &RewriteTermV0,
777    rule_catalog: &RewriteRuleCatalogV0,
778    certificate_json: &str,
779    assumptions: &CanonicalRewriteAssumptionsV0,
780) -> Result<RewriteIssuanceTokenV0, CertificateRejectionV0> {
781    let certificate = serde_json::from_str::<RewriteCertificateEnvelopeV0>(certificate_json)
782        .map_err(|error| {
783            CertificateRejectionV0::new(
784                RewriteFailureSiteV0::root(RewriteCheckInputV0::SerializedCertificate),
785                CertificateRejectionKindV0::MalformedCertificate {
786                    message: error.to_string(),
787                },
788            )
789        })?;
790    check_rewrite_certificate_v0(before, after, rule_catalog, &certificate, assumptions)
791}
792
793pub fn selector_rewrite_rule_catalog_v0() -> RewriteRuleCatalogV0 {
794    RewriteRuleCatalogV0 {
795        schema_version: REWRITE_RULE_CATALOG_SCHEMA_VERSION_V0.to_owned(),
796        operators: vec![
797            RewriteOperatorV0 {
798                operator: "selectorConcat".to_owned(),
799                arity: 2,
800            },
801            RewriteOperatorV0 {
802                operator: "selectorIs".to_owned(),
803                arity: 1,
804            },
805            RewriteOperatorV0 {
806                operator: "selectorWhere".to_owned(),
807                arity: 1,
808            },
809            RewriteOperatorV0 {
810                operator: "selectorList2".to_owned(),
811                arity: 2,
812            },
813        ],
814        rules: vec![
815            RewriteRuleV0 {
816                rule_id: "selector-list-deduplicate-v0".to_owned(),
817                before_pattern: RewritePatternV0::apply(
818                    "selectorList2",
819                    vec![
820                        RewritePatternV0::variable("x"),
821                        RewritePatternV0::variable("x"),
822                    ],
823                ),
824                after_pattern: RewritePatternV0::variable("x"),
825                side_condition_kind: RewriteSideConditionKindV0::NoSideCondition,
826            },
827            RewriteRuleV0 {
828                rule_id: "selector-is-single-v0".to_owned(),
829                before_pattern: RewritePatternV0::apply(
830                    "selectorIs",
831                    vec![RewritePatternV0::variable("x")],
832                ),
833                after_pattern: RewritePatternV0::variable("x"),
834                side_condition_kind: RewriteSideConditionKindV0::NoSideCondition,
835            },
836        ],
837    }
838}
839
840pub fn selector_rewrite_rule_catalog_with_cascade_winner_equality_v0() -> RewriteRuleCatalogV0 {
841    let mut catalog = selector_rewrite_rule_catalog_v0();
842    for rule in &mut catalog.rules {
843        rule.side_condition_kind = RewriteSideConditionKindV0::CascadeWinnerEquality;
844    }
845    catalog
846}
847
848fn validate_schema(
849    observed: &str,
850    expected: &str,
851    input: RewriteCheckInputV0,
852) -> Result<(), CertificateRejectionV0> {
853    if observed == expected {
854        return Ok(());
855    }
856    Err(CertificateRejectionV0::new(
857        RewriteFailureSiteV0::root(input),
858        CertificateRejectionKindV0::SchemaVersionMismatch {
859            expected: expected.to_owned(),
860            observed: observed.to_owned(),
861        },
862    ))
863}
864
865fn validate_certificate_bounds(
866    envelope: &RewriteCertificateEnvelopeV0,
867) -> Result<(), CertificateRejectionV0> {
868    for (bound, declared, hard_maximum) in [
869        (
870            "depth",
871            envelope.max_depth,
872            REWRITE_CERTIFICATE_MAX_DEPTH_V0,
873        ),
874        (
875            "nodes",
876            envelope.max_nodes,
877            REWRITE_CERTIFICATE_MAX_NODES_V0,
878        ),
879    ] {
880        if declared == 0 || declared > hard_maximum {
881            return Err(CertificateRejectionV0::new(
882                RewriteFailureSiteV0::root(RewriteCheckInputV0::Certificate),
883                CertificateRejectionKindV0::BoundOutOfRange {
884                    bound: bound.to_owned(),
885                    declared,
886                    hard_maximum,
887                },
888            ));
889        }
890    }
891
892    let mut stack = vec![(&envelope.certificate, 1_usize, Vec::<usize>::new())];
893    let mut nodes = 0_usize;
894    while let Some((certificate, depth, path)) = stack.pop() {
895        nodes = nodes.saturating_add(1);
896        if depth > envelope.max_depth {
897            return Err(CertificateRejectionV0::new(
898                RewriteFailureSiteV0::certificate(path.as_slice()),
899                CertificateRejectionKindV0::DeclaredBoundExceeded {
900                    bound: "depth".to_owned(),
901                    declared: envelope.max_depth,
902                    observed: depth,
903                },
904            ));
905        }
906        if nodes > envelope.max_nodes {
907            return Err(CertificateRejectionV0::new(
908                RewriteFailureSiteV0::certificate(path.as_slice()),
909                CertificateRejectionKindV0::DeclaredBoundExceeded {
910                    bound: "nodes".to_owned(),
911                    declared: envelope.max_nodes,
912                    observed: nodes,
913                },
914            ));
915        }
916        match certificate {
917            RewriteCertificateV0::Refl { .. } | RewriteCertificateV0::Rewrite { .. } => {}
918            RewriteCertificateV0::Sym { certificate } => {
919                let mut child_path = path;
920                child_path.push(0);
921                stack.push((certificate, depth.saturating_add(1), child_path));
922            }
923            RewriteCertificateV0::Trans { left, right } => {
924                let mut right_path = path.clone();
925                right_path.push(1);
926                stack.push((right, depth.saturating_add(1), right_path));
927                let mut left_path = path;
928                left_path.push(0);
929                stack.push((left, depth.saturating_add(1), left_path));
930            }
931            RewriteCertificateV0::Cong { certificates, .. } => {
932                for (index, child) in certificates.iter().enumerate().rev() {
933                    let mut child_path = path.clone();
934                    child_path.push(index);
935                    stack.push((child, depth.saturating_add(1), child_path));
936                }
937            }
938        }
939    }
940    Ok(())
941}
942
943fn validate_catalog(
944    catalog: &RewriteRuleCatalogV0,
945) -> Result<ValidatedCatalogV0<'_>, CertificateRejectionV0> {
946    validate_schema(
947        &catalog.schema_version,
948        REWRITE_RULE_CATALOG_SCHEMA_VERSION_V0,
949        RewriteCheckInputV0::RuleCatalog,
950    )?;
951    validate_catalog_limit(
952        "operators",
953        catalog.operators.len(),
954        REWRITE_RULE_CATALOG_MAX_OPERATORS_V0,
955    )?;
956    validate_catalog_limit(
957        "rules",
958        catalog.rules.len(),
959        REWRITE_RULE_CATALOG_MAX_RULES_V0,
960    )?;
961
962    let mut operators = BTreeMap::new();
963    for operator in &catalog.operators {
964        if operators
965            .insert(operator.operator.as_str(), operator.arity)
966            .is_some()
967        {
968            return Err(CertificateRejectionV0::new(
969                RewriteFailureSiteV0::root(RewriteCheckInputV0::RuleCatalog),
970                CertificateRejectionKindV0::DuplicateOperator {
971                    operator: operator.operator.clone(),
972                },
973            ));
974        }
975    }
976
977    let mut rules = BTreeMap::new();
978    for rule in &catalog.rules {
979        if rules.insert(rule.rule_id.as_str(), rule).is_some() {
980            return Err(CertificateRejectionV0::new(
981                RewriteFailureSiteV0::rule(&rule.rule_id, Vec::new()),
982                CertificateRejectionKindV0::DuplicateRule {
983                    rule_id: rule.rule_id.clone(),
984                },
985            ));
986        }
987        validate_pattern(&rule.before_pattern, &rule.rule_id, &operators)?;
988        validate_pattern(&rule.after_pattern, &rule.rule_id, &operators)?;
989    }
990    Ok(ValidatedCatalogV0 { operators, rules })
991}
992
993fn validate_catalog_limit(
994    collection: &str,
995    observed: usize,
996    maximum: usize,
997) -> Result<(), CertificateRejectionV0> {
998    if observed <= maximum {
999        return Ok(());
1000    }
1001    Err(CertificateRejectionV0::new(
1002        RewriteFailureSiteV0::root(RewriteCheckInputV0::RuleCatalog),
1003        CertificateRejectionKindV0::CatalogLimitExceeded {
1004            collection: collection.to_owned(),
1005            observed,
1006            maximum,
1007        },
1008    ))
1009}
1010
1011fn validate_pattern(
1012    pattern: &RewritePatternV0,
1013    rule_id: &str,
1014    operators: &BTreeMap<&str, usize>,
1015) -> Result<(), CertificateRejectionV0> {
1016    let mut stack = vec![(pattern, 1_usize, Vec::<usize>::new())];
1017    let mut nodes = 0_usize;
1018    while let Some((current, depth, path)) = stack.pop() {
1019        nodes = nodes.saturating_add(1);
1020        if depth > REWRITE_TERM_MAX_DEPTH_V0 || nodes > REWRITE_TERM_MAX_NODES_V0 {
1021            return Err(CertificateRejectionV0::new(
1022                RewriteFailureSiteV0::rule(rule_id, path),
1023                CertificateRejectionKindV0::DerivedTermLimitExceeded,
1024            ));
1025        }
1026        match current {
1027            RewritePatternV0::Atom { .. } => {}
1028            RewritePatternV0::Variable { name } => {
1029                if name.is_empty() {
1030                    return Err(CertificateRejectionV0::new(
1031                        RewriteFailureSiteV0::rule(rule_id, path),
1032                        CertificateRejectionKindV0::EmptyVariable,
1033                    ));
1034                }
1035            }
1036            RewritePatternV0::Apply { operator, operands } => {
1037                validate_operator_arity(
1038                    operator,
1039                    operands.len(),
1040                    operators,
1041                    RewriteFailureSiteV0::rule(rule_id, path.clone()),
1042                )?;
1043                for (index, child) in operands.iter().enumerate().rev() {
1044                    let mut child_path = path.clone();
1045                    child_path.push(index);
1046                    stack.push((child, depth.saturating_add(1), child_path));
1047                }
1048            }
1049        }
1050    }
1051    Ok(())
1052}
1053
1054fn validate_assumptions(
1055    assumptions: &CanonicalRewriteAssumptionsV0,
1056) -> Result<BTreeMap<&str, &str>, CertificateRejectionV0> {
1057    validate_schema(
1058        &assumptions.schema_version,
1059        CANONICAL_REWRITE_ASSUMPTIONS_SCHEMA_VERSION_V0,
1060        RewriteCheckInputV0::Assumptions,
1061    )?;
1062    let mut canonical = BTreeMap::new();
1063    for assumption in &assumptions.entries {
1064        if canonical
1065            .insert(assumption.name.as_str(), assumption.value.as_str())
1066            .is_some()
1067        {
1068            return Err(CertificateRejectionV0::new(
1069                RewriteFailureSiteV0::root(RewriteCheckInputV0::Assumptions),
1070                CertificateRejectionKindV0::DuplicateAssumption {
1071                    name: assumption.name.clone(),
1072                },
1073            ));
1074        }
1075    }
1076    Ok(canonical)
1077}
1078
1079fn validate_term(
1080    term: &RewriteTermV0,
1081    input: RewriteCheckInputV0,
1082    certificate_path: &[usize],
1083    operators: &BTreeMap<&str, usize>,
1084) -> Result<(), CertificateRejectionV0> {
1085    let mut stack = vec![(term, 1_usize, Vec::<usize>::new())];
1086    let mut nodes = 0_usize;
1087    while let Some((current, depth, term_path)) = stack.pop() {
1088        nodes = nodes.saturating_add(1);
1089        if depth > REWRITE_TERM_MAX_DEPTH_V0 || nodes > REWRITE_TERM_MAX_NODES_V0 {
1090            return Err(CertificateRejectionV0::new(
1091                RewriteFailureSiteV0 {
1092                    input,
1093                    certificate_path: certificate_path.to_vec(),
1094                    term_path,
1095                    rule_id: None,
1096                },
1097                CertificateRejectionKindV0::DerivedTermLimitExceeded,
1098            ));
1099        }
1100        if let RewriteTermV0::Apply { operator, operands } = current {
1101            validate_operator_arity(
1102                operator,
1103                operands.len(),
1104                operators,
1105                RewriteFailureSiteV0 {
1106                    input,
1107                    certificate_path: certificate_path.to_vec(),
1108                    term_path: term_path.clone(),
1109                    rule_id: None,
1110                },
1111            )?;
1112            for (index, child) in operands.iter().enumerate().rev() {
1113                let mut child_path = term_path.clone();
1114                child_path.push(index);
1115                stack.push((child, depth.saturating_add(1), child_path));
1116            }
1117        }
1118    }
1119    Ok(())
1120}
1121
1122fn validate_operator_arity(
1123    operator: &str,
1124    observed: usize,
1125    operators: &BTreeMap<&str, usize>,
1126    site: RewriteFailureSiteV0,
1127) -> Result<(), CertificateRejectionV0> {
1128    let Some(expected) = operators.get(operator).copied() else {
1129        return Err(CertificateRejectionV0::new(
1130            site,
1131            CertificateRejectionKindV0::UnknownOperator {
1132                operator: operator.to_owned(),
1133            },
1134        ));
1135    };
1136    if expected == observed {
1137        return Ok(());
1138    }
1139    Err(CertificateRejectionV0::new(
1140        site,
1141        CertificateRejectionKindV0::OperatorArityMismatch {
1142            operator: operator.to_owned(),
1143            expected,
1144            observed,
1145        },
1146    ))
1147}
1148
1149fn derive_certificate(
1150    certificate: &RewriteCertificateV0,
1151    catalog: &ValidatedCatalogV0<'_>,
1152    path: &mut Vec<usize>,
1153) -> Result<DerivedRewriteV0, CertificateRejectionV0> {
1154    match certificate {
1155        RewriteCertificateV0::Refl { term } => {
1156            validate_term(
1157                term,
1158                RewriteCheckInputV0::Certificate,
1159                path,
1160                &catalog.operators,
1161            )?;
1162            Ok(DerivedRewriteV0 {
1163                before: term.clone(),
1164                after: term.clone(),
1165                checked_rule_ids: Vec::new(),
1166            })
1167        }
1168        RewriteCertificateV0::Sym { certificate } => {
1169            path.push(0);
1170            let child = derive_certificate(certificate, catalog, path);
1171            path.pop();
1172            let child = child?;
1173            Ok(DerivedRewriteV0 {
1174                before: child.after,
1175                after: child.before,
1176                checked_rule_ids: child.checked_rule_ids,
1177            })
1178        }
1179        RewriteCertificateV0::Trans { left, right } => {
1180            path.push(0);
1181            let left_derived = derive_certificate(left, catalog, path);
1182            path.pop();
1183            let left_derived = left_derived?;
1184            path.push(1);
1185            let right_derived = derive_certificate(right, catalog, path);
1186            path.pop();
1187            let right_derived = right_derived?;
1188            if left_derived.after != right_derived.before {
1189                return Err(CertificateRejectionV0::new(
1190                    RewriteFailureSiteV0 {
1191                        input: RewriteCheckInputV0::Certificate,
1192                        certificate_path: path.clone(),
1193                        term_path: first_term_mismatch_path(
1194                            &left_derived.after,
1195                            &right_derived.before,
1196                        ),
1197                        rule_id: None,
1198                    },
1199                    CertificateRejectionKindV0::TransitiveMiddleMismatch,
1200                ));
1201            }
1202            let mut checked_rule_ids = left_derived.checked_rule_ids;
1203            checked_rule_ids.extend(right_derived.checked_rule_ids);
1204            Ok(DerivedRewriteV0 {
1205                before: left_derived.before,
1206                after: right_derived.after,
1207                checked_rule_ids,
1208            })
1209        }
1210        RewriteCertificateV0::Cong {
1211            operator,
1212            certificates,
1213        } => {
1214            validate_operator_arity(
1215                operator,
1216                certificates.len(),
1217                &catalog.operators,
1218                RewriteFailureSiteV0::certificate(path.as_slice()),
1219            )?;
1220            let mut before_operands = Vec::with_capacity(certificates.len());
1221            let mut after_operands = Vec::with_capacity(certificates.len());
1222            let mut checked_rule_ids = Vec::new();
1223            for (index, child) in certificates.iter().enumerate() {
1224                path.push(index);
1225                let child_derived = derive_certificate(child, catalog, path);
1226                path.pop();
1227                let child_derived = child_derived?;
1228                before_operands.push(child_derived.before);
1229                after_operands.push(child_derived.after);
1230                checked_rule_ids.extend(child_derived.checked_rule_ids);
1231            }
1232            let before = RewriteTermV0::apply(operator, before_operands);
1233            let after = RewriteTermV0::apply(operator, after_operands);
1234            validate_term(
1235                &before,
1236                RewriteCheckInputV0::Certificate,
1237                path,
1238                &catalog.operators,
1239            )?;
1240            validate_term(
1241                &after,
1242                RewriteCheckInputV0::Certificate,
1243                path,
1244                &catalog.operators,
1245            )?;
1246            Ok(DerivedRewriteV0 {
1247                before,
1248                after,
1249                checked_rule_ids,
1250            })
1251        }
1252        RewriteCertificateV0::Rewrite {
1253            rule_id,
1254            substitution,
1255            side_condition,
1256        } => derive_rule_application(rule_id, substitution, side_condition, catalog, path),
1257    }
1258}
1259
1260fn derive_rule_application(
1261    rule_id: &str,
1262    substitution: &[RewriteSubstitutionEntryV0],
1263    side_condition: &SideConditionCertV0,
1264    catalog: &ValidatedCatalogV0<'_>,
1265    path: &[usize],
1266) -> Result<DerivedRewriteV0, CertificateRejectionV0> {
1267    let Some(rule) = catalog.rules.get(rule_id).copied() else {
1268        return Err(CertificateRejectionV0::new(
1269            RewriteFailureSiteV0 {
1270                input: RewriteCheckInputV0::Certificate,
1271                certificate_path: path.to_vec(),
1272                term_path: Vec::new(),
1273                rule_id: Some(rule_id.to_owned()),
1274            },
1275            CertificateRejectionKindV0::UnknownRule {
1276                rule_id: rule_id.to_owned(),
1277            },
1278        ));
1279    };
1280    let observed_kind = side_condition.kind();
1281    if observed_kind != rule.side_condition_kind {
1282        return Err(CertificateRejectionV0::new(
1283            RewriteFailureSiteV0 {
1284                input: RewriteCheckInputV0::Certificate,
1285                certificate_path: path.to_vec(),
1286                term_path: Vec::new(),
1287                rule_id: Some(rule_id.to_owned()),
1288            },
1289            CertificateRejectionKindV0::SideConditionKindMismatch {
1290                expected: rule.side_condition_kind,
1291                observed: observed_kind,
1292            },
1293        ));
1294    }
1295    check_side_condition_v0(side_condition, path, rule_id)?;
1296
1297    let mut substitutions = BTreeMap::new();
1298    for entry in substitution {
1299        if substitutions
1300            .insert(entry.variable.as_str(), &entry.term)
1301            .is_some()
1302        {
1303            return Err(CertificateRejectionV0::new(
1304                RewriteFailureSiteV0 {
1305                    input: RewriteCheckInputV0::Certificate,
1306                    certificate_path: path.to_vec(),
1307                    term_path: Vec::new(),
1308                    rule_id: Some(rule_id.to_owned()),
1309                },
1310                CertificateRejectionKindV0::DuplicateSubstitutionVariable {
1311                    variable: entry.variable.clone(),
1312                },
1313            ));
1314        }
1315        validate_term(
1316            &entry.term,
1317            RewriteCheckInputV0::Certificate,
1318            path,
1319            &catalog.operators,
1320        )?;
1321    }
1322
1323    let mut variables = BTreeSet::new();
1324    collect_pattern_variables(&rule.before_pattern, &mut variables);
1325    collect_pattern_variables(&rule.after_pattern, &mut variables);
1326    if let Some(missing) = variables
1327        .iter()
1328        .find(|variable| !substitutions.contains_key(variable.as_str()))
1329    {
1330        return Err(CertificateRejectionV0::new(
1331            RewriteFailureSiteV0 {
1332                input: RewriteCheckInputV0::Certificate,
1333                certificate_path: path.to_vec(),
1334                term_path: Vec::new(),
1335                rule_id: Some(rule_id.to_owned()),
1336            },
1337            CertificateRejectionKindV0::MissingSubstitutionVariable {
1338                variable: (*missing).clone(),
1339            },
1340        ));
1341    }
1342    if let Some(extra) = substitutions
1343        .keys()
1344        .find(|variable| !variables.contains(**variable))
1345    {
1346        return Err(CertificateRejectionV0::new(
1347            RewriteFailureSiteV0 {
1348                input: RewriteCheckInputV0::Certificate,
1349                certificate_path: path.to_vec(),
1350                term_path: Vec::new(),
1351                rule_id: Some(rule_id.to_owned()),
1352            },
1353            CertificateRejectionKindV0::UnexpectedSubstitutionVariable {
1354                variable: (*extra).to_owned(),
1355            },
1356        ));
1357    }
1358
1359    let mut before_nodes = 0_usize;
1360    let before = instantiate_pattern(&rule.before_pattern, &substitutions, &mut before_nodes)
1361        .ok_or_else(|| {
1362            CertificateRejectionV0::new(
1363                RewriteFailureSiteV0 {
1364                    input: RewriteCheckInputV0::Certificate,
1365                    certificate_path: path.to_vec(),
1366                    term_path: Vec::new(),
1367                    rule_id: Some(rule_id.to_owned()),
1368                },
1369                CertificateRejectionKindV0::DerivedTermLimitExceeded,
1370            )
1371        })?;
1372    let mut after_nodes = 0_usize;
1373    let after = instantiate_pattern(&rule.after_pattern, &substitutions, &mut after_nodes)
1374        .ok_or_else(|| {
1375            CertificateRejectionV0::new(
1376                RewriteFailureSiteV0 {
1377                    input: RewriteCheckInputV0::Certificate,
1378                    certificate_path: path.to_vec(),
1379                    term_path: Vec::new(),
1380                    rule_id: Some(rule_id.to_owned()),
1381                },
1382                CertificateRejectionKindV0::DerivedTermLimitExceeded,
1383            )
1384        })?;
1385    Ok(DerivedRewriteV0 {
1386        before,
1387        after,
1388        checked_rule_ids: vec![rule_id.to_owned()],
1389    })
1390}
1391
1392fn check_side_condition_v0(
1393    side_condition: &SideConditionCertV0,
1394    path: &[usize],
1395    rule_id: &str,
1396) -> Result<(), CertificateRejectionV0> {
1397    match side_condition {
1398        SideConditionCertV0::NoSideCondition => Ok(()),
1399        SideConditionCertV0::CascadeWinnerEquality { certificate } => {
1400            check_cascade_winner_equality_v0(certificate, path, rule_id)
1401        }
1402        SideConditionCertV0::ComputedValueEquality { certificate } => {
1403            check_computed_value_equality_v0(certificate, path, rule_id)
1404        }
1405        SideConditionCertV0::SourceMapTrace { certificate } => {
1406            check_source_map_trace_v0(certificate, path, rule_id)
1407        }
1408        SideConditionCertV0::TokenOwnershipSeparability { certificate } => {
1409            check_token_ownership_separability_v0(certificate, path, rule_id)
1410        }
1411        SideConditionCertV0::TransformIndependence { certificate } => {
1412            check_transform_independence_v0(certificate, path, rule_id)
1413        }
1414    }
1415}
1416
1417fn check_transform_independence_v0(
1418    certificate: &TransformIndependenceCertV0,
1419    path: &[usize],
1420    rule_id: &str,
1421) -> Result<(), CertificateRejectionV0> {
1422    let reject = |reason: &str| {
1423        CertificateRejectionV0::new(
1424            side_condition_site_v0(path, rule_id),
1425            CertificateRejectionKindV0::TransformIndependenceRejected {
1426                reason: reason.to_owned(),
1427                left_pass_id: certificate.left_pass_id.clone(),
1428                right_pass_id: certificate.right_pass_id.clone(),
1429            },
1430        )
1431    };
1432    if certificate.left_pass_id.is_empty()
1433        || certificate.right_pass_id.is_empty()
1434        || certificate.left_pass_id == certificate.right_pass_id
1435    {
1436        return Err(reject("independence pair is empty or reflexive"));
1437    }
1438    if certificate.observation_profile_id.is_empty() || certificate.profile_observers.is_empty() {
1439        return Err(reject("observation profile is empty"));
1440    }
1441    if !certificate.disqualifying_descriptor_edges.is_empty() {
1442        return Err(reject(
1443            "descriptor dependency or conflict disqualifies the pair",
1444        ));
1445    }
1446
1447    let profile_observers = certificate
1448        .profile_observers
1449        .iter()
1450        .map(String::as_str)
1451        .collect::<BTreeSet<_>>();
1452    if profile_observers.len() != certificate.profile_observers.len()
1453        || profile_observers.contains("")
1454    {
1455        return Err(reject(
1456            "observation profile contains an empty or duplicate observer",
1457        ));
1458    }
1459    if certificate.observation_rows.is_empty() {
1460        return Err(reject("observational commutation has no checked rows"));
1461    }
1462    let mut observed_profile_members = BTreeSet::new();
1463    let mut row_keys = BTreeSet::new();
1464    for row in &certificate.observation_rows {
1465        if row.fixture_id.is_empty() || !profile_observers.contains(row.observer.as_str()) {
1466            return Err(reject(
1467                "observation row does not resolve to the named profile",
1468            ));
1469        }
1470        if !row_keys.insert((row.fixture_id.as_str(), row.observer.as_str())) {
1471            return Err(reject("observation row is duplicated"));
1472        }
1473        if row.left_then_right != row.right_then_left {
1474            return Err(reject(
1475                "adjacent transform orders have different observations",
1476            ));
1477        }
1478        observed_profile_members.insert(row.observer.as_str());
1479    }
1480    if observed_profile_members != profile_observers {
1481        return Err(reject("observation rows do not cover the named profile"));
1482    }
1483
1484    let left_preconditions = certificate
1485        .left_preconditions
1486        .iter()
1487        .map(String::as_str)
1488        .collect::<BTreeSet<_>>();
1489    let right_preconditions = certificate
1490        .right_preconditions
1491        .iter()
1492        .map(String::as_str)
1493        .collect::<BTreeSet<_>>();
1494    let left_preserves_right = certificate
1495        .left_preserves_right_preconditions
1496        .iter()
1497        .map(String::as_str)
1498        .collect::<BTreeSet<_>>();
1499    let right_preserves_left = certificate
1500        .right_preserves_left_preconditions
1501        .iter()
1502        .map(String::as_str)
1503        .collect::<BTreeSet<_>>();
1504    if left_preconditions.len() != certificate.left_preconditions.len()
1505        || right_preconditions.len() != certificate.right_preconditions.len()
1506        || left_preserves_right.len() != certificate.left_preserves_right_preconditions.len()
1507        || right_preserves_left.len() != certificate.right_preserves_left_preconditions.len()
1508    {
1509        return Err(reject("precondition evidence contains duplicate entries"));
1510    }
1511    if left_preserves_right != right_preconditions || right_preserves_left != left_preconditions {
1512        return Err(reject("mutual precondition preservation is incomplete"));
1513    }
1514    Ok(())
1515}
1516
1517fn check_token_ownership_separability_v0(
1518    certificate: &TokenOwnershipSeparabilityCertV0,
1519    path: &[usize],
1520    rule_id: &str,
1521) -> Result<(), CertificateRejectionV0> {
1522    let reject = |reason: &str, token: Option<String>| {
1523        CertificateRejectionV0::new(
1524            side_condition_site_v0(path, rule_id),
1525            CertificateRejectionKindV0::TokenOwnershipSeparabilityRejected {
1526                reason: reason.to_owned(),
1527                token,
1528            },
1529        )
1530    };
1531    if !certificate.complete {
1532        return Err(reject("ownership census is incomplete", None));
1533    }
1534    if certificate.unattributed_emitted_token_count != 0 {
1535        return Err(reject("emitted token has no attributed owner", None));
1536    }
1537    if certificate.interface_mismatch_count != 0 {
1538        return Err(reject("emitted token disagrees with its interface", None));
1539    }
1540    if certificate.emitted_token_count != certificate.ownerships.len() {
1541        return Err(reject(
1542            "emitted token count does not match ownership rows",
1543            None,
1544        ));
1545    }
1546    let mut tokens = BTreeSet::new();
1547    for ownership in &certificate.ownerships {
1548        if ownership.emitted_token.is_empty() {
1549            return Err(reject("ownership row has an empty emitted token", None));
1550        }
1551        if !tokens.insert(ownership.emitted_token.as_str()) {
1552            return Err(reject(
1553                "ownership census repeats an emitted token",
1554                Some(ownership.emitted_token.clone()),
1555            ));
1556        }
1557        if ownership.module_paths.len() != 1 || ownership.module_paths[0].is_empty() {
1558            return Err(reject(
1559                "emitted token does not resolve to exactly one module path",
1560                Some(ownership.emitted_token.clone()),
1561            ));
1562        }
1563    }
1564    if certificate.modeled_preimage_count != certificate.ownerships.len() {
1565        return Err(reject(
1566            "modeled preimages do not form a one-to-one ownership relation",
1567            None,
1568        ));
1569    }
1570    Ok(())
1571}
1572
1573fn side_condition_site_v0(path: &[usize], rule_id: &str) -> RewriteFailureSiteV0 {
1574    RewriteFailureSiteV0 {
1575        input: RewriteCheckInputV0::Certificate,
1576        certificate_path: path.to_vec(),
1577        term_path: Vec::new(),
1578        rule_id: Some(rule_id.to_owned()),
1579    }
1580}
1581
1582fn cascade_key_from_certificate_v0(
1583    key: &CascadeWinnerKeyCertV0,
1584    path: &[usize],
1585    rule_id: &str,
1586) -> Result<CascadeKey, CertificateRejectionV0> {
1587    let layer_ordinal = match key.layer_ordinal {
1588        Some(ordinal) => Some(LayerOrdinal::new(ordinal).ok_or_else(|| {
1589            CertificateRejectionV0::new(
1590                side_condition_site_v0(path, rule_id),
1591                CertificateRejectionKindV0::InvalidLayerOrdinal { observed: ordinal },
1592            )
1593        })?),
1594        None => None,
1595    };
1596    Ok(CascadeKey::new(
1597        key.level.to_cascade_level(),
1598        normalized_layer_rank(key.layer_important, layer_ordinal),
1599        key.scope_proximity,
1600        Specificity::new(
1601            key.specificity_ids,
1602            key.specificity_classes,
1603            key.specificity_elements,
1604        ),
1605        key.source_order,
1606    ))
1607}
1608
1609fn check_cascade_winner_equality_v0(
1610    certificate: &CascadeWinnerEqualityCertV0,
1611    path: &[usize],
1612    rule_id: &str,
1613) -> Result<(), CertificateRejectionV0> {
1614    // The key ordering and token support are recomputed by omena-cascade,
1615    // outside the transform pass that benefits from this certificate.
1616    let before_key = cascade_key_from_certificate_v0(&certificate.before_key, path, rule_id)?;
1617    let after_key = cascade_key_from_certificate_v0(&certificate.after_key, path, rule_id)?;
1618    let before_tokenization =
1619        tokenize_dom_class_attribute_v0(Some(&certificate.before_class_attribute));
1620    let after_tokenization =
1621        tokenize_dom_class_attribute_v0(Some(&certificate.after_class_attribute));
1622    let (
1623        DomClassTokenizationV0::Known {
1624            word: before_word, ..
1625        },
1626        DomClassTokenizationV0::Known {
1627            word: after_word, ..
1628        },
1629    ) = (before_tokenization, after_tokenization)
1630    else {
1631        return Err(CertificateRejectionV0::new(
1632            side_condition_site_v0(path, rule_id),
1633            CertificateRejectionKindV0::CascadeTokenizationUnavailable,
1634        ));
1635    };
1636    let winner_ids_equal = certificate.before_winner_id == certificate.after_winner_id;
1637    let cascade_keys_equal = before_key == after_key;
1638    let token_support_equal = token_support_v0(&before_word) == token_support_v0(&after_word);
1639    if winner_ids_equal && cascade_keys_equal && token_support_equal {
1640        return Ok(());
1641    }
1642    Err(CertificateRejectionV0::new(
1643        side_condition_site_v0(path, rule_id),
1644        CertificateRejectionKindV0::CascadeWinnerEqualityRejected {
1645            winner_ids_equal,
1646            cascade_keys_equal,
1647            token_support_equal,
1648        },
1649    ))
1650}
1651
1652fn computed_value_environment_v0(
1653    entries: &[ComputedValueEnvironmentEntryV0],
1654    path: &[usize],
1655    rule_id: &str,
1656) -> Result<BTreeMap<String, CascadeValue>, CertificateRejectionV0> {
1657    let mut environment = BTreeMap::new();
1658    for entry in entries {
1659        if environment
1660            .insert(entry.name.clone(), entry.value.to_cascade_value())
1661            .is_some()
1662        {
1663            return Err(CertificateRejectionV0::new(
1664                side_condition_site_v0(path, rule_id),
1665                CertificateRejectionKindV0::DuplicateComputedValueEnvironmentEntry {
1666                    name: entry.name.clone(),
1667                },
1668            ));
1669        }
1670    }
1671    Ok(environment)
1672}
1673
1674fn check_computed_value_equality_v0(
1675    certificate: &ComputedValueEqualityCertV0,
1676    path: &[usize],
1677    rule_id: &str,
1678) -> Result<(), CertificateRejectionV0> {
1679    // Fixed-point resolution is recomputed by omena-cascade's value plane,
1680    // outside the transform pass and outside the obligation-family tag.
1681    let before_environment =
1682        computed_value_environment_v0(&certificate.before_environment, path, rule_id)?;
1683    let after_environment =
1684        computed_value_environment_v0(&certificate.after_environment, path, rule_id)?;
1685    let before_resolved = resolve_custom_property_env_least_fixed_point(&before_environment);
1686    let after_resolved = resolve_custom_property_env_least_fixed_point(&after_environment);
1687    let before_value = before_resolved.get(&certificate.property);
1688    let after_value = after_resolved.get(&certificate.property);
1689    if before_value.is_some() && before_value == after_value {
1690        return Ok(());
1691    }
1692    Err(CertificateRejectionV0::new(
1693        side_condition_site_v0(path, rule_id),
1694        CertificateRejectionKindV0::ComputedValueEqualityRejected {
1695            property: certificate.property.clone(),
1696            before_present: before_value.is_some(),
1697            after_present: after_value.is_some(),
1698        },
1699    ))
1700}
1701
1702fn check_source_map_trace_v0(
1703    certificate: &SourceMapTraceCertV0,
1704    path: &[usize],
1705    rule_id: &str,
1706) -> Result<(), CertificateRejectionV0> {
1707    // These are emitted segment-table records, not a transform-owned
1708    // provenance boolean. The checker compares their source projections.
1709    if certificate.before_segments.is_empty() || certificate.after_segments.is_empty() {
1710        return Err(CertificateRejectionV0::new(
1711            side_condition_site_v0(path, rule_id),
1712            CertificateRejectionKindV0::EmptySourceMapTrace,
1713        ));
1714    }
1715    if certificate.before_segments.len() != certificate.after_segments.len() {
1716        return Err(CertificateRejectionV0::new(
1717            side_condition_site_v0(path, rule_id),
1718            CertificateRejectionKindV0::SourceMapTraceRejected {
1719                segment_index: 0,
1720                reason: "segment count differs".to_owned(),
1721            },
1722        ));
1723    }
1724    for (index, (before, after)) in certificate
1725        .before_segments
1726        .iter()
1727        .zip(&certificate.after_segments)
1728        .enumerate()
1729    {
1730        let ranges_valid = before.original_start <= before.original_end
1731            && before.generated_start <= before.generated_end
1732            && after.original_start <= after.original_end
1733            && after.generated_start <= after.generated_end;
1734        let source_projection_equal = before.source_path == after.source_path
1735            && before.source_digest == after.source_digest
1736            && before.original_start == after.original_start
1737            && before.original_end == after.original_end
1738            && before.pass_id == after.pass_id;
1739        if !ranges_valid || !source_projection_equal {
1740            return Err(CertificateRejectionV0::new(
1741                side_condition_site_v0(path, rule_id),
1742                CertificateRejectionKindV0::SourceMapTraceRejected {
1743                    segment_index: index,
1744                    reason: if ranges_valid {
1745                        "source projection differs".to_owned()
1746                    } else {
1747                        "segment range is inverted".to_owned()
1748                    },
1749                },
1750            ));
1751        }
1752    }
1753    Ok(())
1754}
1755
1756fn collect_pattern_variables(pattern: &RewritePatternV0, variables: &mut BTreeSet<String>) {
1757    let mut stack = vec![pattern];
1758    while let Some(current) = stack.pop() {
1759        match current {
1760            RewritePatternV0::Atom { .. } => {}
1761            RewritePatternV0::Variable { name } => {
1762                variables.insert(name.clone());
1763            }
1764            RewritePatternV0::Apply { operands, .. } => stack.extend(operands),
1765        }
1766    }
1767}
1768
1769fn instantiate_pattern(
1770    pattern: &RewritePatternV0,
1771    substitutions: &BTreeMap<&str, &RewriteTermV0>,
1772    nodes: &mut usize,
1773) -> Option<RewriteTermV0> {
1774    *nodes = nodes.saturating_add(1);
1775    if *nodes > REWRITE_TERM_MAX_NODES_V0 {
1776        return None;
1777    }
1778    match pattern {
1779        RewritePatternV0::Atom { value } => Some(RewriteTermV0::atom(value)),
1780        RewritePatternV0::Variable { name } => {
1781            let term = substitutions.get(name.as_str())?;
1782            let term_nodes = term_node_count_v0(term)?;
1783            *nodes = nodes.saturating_add(term_nodes.saturating_sub(1));
1784            (*nodes <= REWRITE_TERM_MAX_NODES_V0).then(|| (*term).clone())
1785        }
1786        RewritePatternV0::Apply { operator, operands } => {
1787            let mut instantiated = Vec::with_capacity(operands.len());
1788            for operand in operands {
1789                instantiated.push(instantiate_pattern(operand, substitutions, nodes)?);
1790            }
1791            Some(RewriteTermV0::apply(operator, instantiated))
1792        }
1793    }
1794}
1795
1796fn term_node_count_v0(term: &RewriteTermV0) -> Option<usize> {
1797    let mut stack = vec![term];
1798    let mut nodes = 0_usize;
1799    while let Some(current) = stack.pop() {
1800        nodes = nodes.checked_add(1)?;
1801        if nodes > REWRITE_TERM_MAX_NODES_V0 {
1802            return None;
1803        }
1804        if let RewriteTermV0::Apply { operands, .. } = current {
1805            stack.extend(operands);
1806        }
1807    }
1808    Some(nodes)
1809}
1810
1811fn first_term_mismatch_path(expected: &RewriteTermV0, observed: &RewriteTermV0) -> Vec<usize> {
1812    let mut stack = vec![(expected, observed, Vec::<usize>::new())];
1813    while let Some((left, right, path)) = stack.pop() {
1814        match (left, right) {
1815            (RewriteTermV0::Atom { value: left }, RewriteTermV0::Atom { value: right })
1816                if left == right => {}
1817            (
1818                RewriteTermV0::Apply {
1819                    operator: left_operator,
1820                    operands: left_operands,
1821                },
1822                RewriteTermV0::Apply {
1823                    operator: right_operator,
1824                    operands: right_operands,
1825                },
1826            ) if left_operator == right_operator && left_operands.len() == right_operands.len() => {
1827                for index in (0..left_operands.len()).rev() {
1828                    let mut child_path = path.clone();
1829                    child_path.push(index);
1830                    stack.push((&left_operands[index], &right_operands[index], child_path));
1831                }
1832            }
1833            _ => return path,
1834        }
1835    }
1836    Vec::new()
1837}
1838
1839/// Return the order-independent digest used to bind an issuance token to the
1840/// exact catalog content checked by the kernel.
1841pub fn rewrite_rule_catalog_content_digest_hex_v0(catalog: &RewriteRuleCatalogV0) -> String {
1842    digest_hex_v0(&catalog_content_digest_v0(catalog))
1843}
1844
1845fn catalog_content_digest_v0(catalog: &RewriteRuleCatalogV0) -> [u8; 32] {
1846    let mut operators = catalog
1847        .operators
1848        .iter()
1849        .map(|operator| {
1850            let mut material = Vec::new();
1851            append_framed_bytes_v0(&mut material, operator.operator.as_bytes());
1852            append_framed_bytes_v0(&mut material, operator.arity.to_string().as_bytes());
1853            material
1854        })
1855        .collect::<Vec<_>>();
1856    operators.sort();
1857
1858    let mut rules = catalog
1859        .rules
1860        .iter()
1861        .map(|rule| {
1862            let mut material = Vec::new();
1863            append_framed_bytes_v0(&mut material, rule.rule_id.as_bytes());
1864            append_pattern_material_v0(&mut material, &rule.before_pattern);
1865            append_pattern_material_v0(&mut material, &rule.after_pattern);
1866            append_framed_bytes_v0(
1867                &mut material,
1868                rewrite_side_condition_kind_id_v0(rule.side_condition_kind).as_bytes(),
1869            );
1870            material
1871        })
1872        .collect::<Vec<_>>();
1873    rules.sort();
1874
1875    let mut hasher = blake3::Hasher::new();
1876    update_framed_hash_v0(&mut hasher, REWRITE_RULE_CATALOG_SCHEMA_ID_V0.as_bytes());
1877    update_framed_hash_v0(&mut hasher, catalog.schema_version.as_bytes());
1878    update_framed_hash_v0(&mut hasher, operators.len().to_string().as_bytes());
1879    for operator in operators {
1880        update_framed_hash_v0(&mut hasher, operator.as_slice());
1881    }
1882    update_framed_hash_v0(&mut hasher, rules.len().to_string().as_bytes());
1883    for rule in rules {
1884        update_framed_hash_v0(&mut hasher, rule.as_slice());
1885    }
1886    *hasher.finalize().as_bytes()
1887}
1888
1889fn append_pattern_material_v0(material: &mut Vec<u8>, pattern: &RewritePatternV0) {
1890    match pattern {
1891        RewritePatternV0::Atom { value } => {
1892            append_framed_bytes_v0(material, b"atom");
1893            append_framed_bytes_v0(material, value.as_bytes());
1894        }
1895        RewritePatternV0::Variable { name } => {
1896            append_framed_bytes_v0(material, b"variable");
1897            append_framed_bytes_v0(material, name.as_bytes());
1898        }
1899        RewritePatternV0::Apply { operator, operands } => {
1900            append_framed_bytes_v0(material, b"apply");
1901            append_framed_bytes_v0(material, operator.as_bytes());
1902            append_framed_bytes_v0(material, operands.len().to_string().as_bytes());
1903            for operand in operands {
1904                append_pattern_material_v0(material, operand);
1905            }
1906        }
1907    }
1908}
1909
1910fn rewrite_side_condition_kind_id_v0(kind: RewriteSideConditionKindV0) -> &'static str {
1911    match kind {
1912        RewriteSideConditionKindV0::NoSideCondition => "noSideCondition",
1913        RewriteSideConditionKindV0::CascadeWinnerEquality => "cascadeWinnerEquality",
1914        RewriteSideConditionKindV0::ComputedValueEquality => "computedValueEquality",
1915        RewriteSideConditionKindV0::SourceMapTrace => "sourceMapTrace",
1916        RewriteSideConditionKindV0::TokenOwnershipSeparability => "tokenOwnershipSeparability",
1917        RewriteSideConditionKindV0::TransformIndependence => "transformIndependence",
1918    }
1919}
1920
1921fn append_framed_bytes_v0(material: &mut Vec<u8>, value: &[u8]) {
1922    material.extend_from_slice(value.len().to_string().as_bytes());
1923    material.push(0);
1924    material.extend_from_slice(value);
1925    material.push(0xff);
1926}
1927
1928fn update_framed_hash_v0(hasher: &mut blake3::Hasher, value: &[u8]) {
1929    hasher.update(value.len().to_string().as_bytes());
1930    hasher.update(b"\0");
1931    hasher.update(value);
1932    hasher.update(b"\xff");
1933}
1934
1935fn term_digest_v0(term: &RewriteTermV0) -> [u8; 32] {
1936    let mut hasher = blake3::Hasher::new();
1937    let mut stack = vec![term];
1938    while let Some(current) = stack.pop() {
1939        match current {
1940            RewriteTermV0::Atom { value } => {
1941                hasher.update(b"atom\0");
1942                hasher.update(value.len().to_string().as_bytes());
1943                hasher.update(b"\0");
1944                hasher.update(value.as_bytes());
1945            }
1946            RewriteTermV0::Apply { operator, operands } => {
1947                hasher.update(b"apply\0");
1948                hasher.update(operator.len().to_string().as_bytes());
1949                hasher.update(b"\0");
1950                hasher.update(operator.as_bytes());
1951                hasher.update(b"\0");
1952                hasher.update(operands.len().to_string().as_bytes());
1953                for operand in operands.iter().rev() {
1954                    stack.push(operand);
1955                }
1956            }
1957        }
1958    }
1959    *hasher.finalize().as_bytes()
1960}
1961
1962fn digest_hex_v0(digest: &[u8; 32]) -> String {
1963    digest.iter().map(|byte| format!("{byte:02x}")).collect()
1964}
1965
1966#[cfg(test)]
1967mod tests {
1968    use std::panic::{AssertUnwindSafe, catch_unwind};
1969
1970    use super::*;
1971
1972    fn selector_terms() -> (RewriteTermV0, RewriteTermV0) {
1973        let before = RewriteTermV0::apply(
1974            "selectorConcat",
1975            vec![
1976                RewriteTermV0::atom(".root"),
1977                RewriteTermV0::apply(
1978                    "selectorIs",
1979                    vec![RewriteTermV0::apply(
1980                        "selectorList2",
1981                        vec![RewriteTermV0::atom(".a"), RewriteTermV0::atom(".a")],
1982                    )],
1983                ),
1984            ],
1985        );
1986        let after = RewriteTermV0::apply(
1987            "selectorConcat",
1988            vec![RewriteTermV0::atom(".root"), RewriteTermV0::atom(".a")],
1989        );
1990        (before, after)
1991    }
1992
1993    fn substitution(variable: &str, value: &str) -> RewriteSubstitutionEntryV0 {
1994        RewriteSubstitutionEntryV0 {
1995            variable: variable.to_owned(),
1996            term: RewriteTermV0::atom(value),
1997        }
1998    }
1999
2000    fn selector_certificate() -> RewriteCertificateEnvelopeV0 {
2001        let deduplicate = RewriteCertificateV0::Rewrite {
2002            rule_id: "selector-list-deduplicate-v0".to_owned(),
2003            substitution: vec![substitution("x", ".a")],
2004            side_condition: SideConditionCertV0::NoSideCondition,
2005        };
2006        let inside_is = RewriteCertificateV0::Trans {
2007            left: Box::new(RewriteCertificateV0::Cong {
2008                operator: "selectorIs".to_owned(),
2009                certificates: vec![deduplicate],
2010            }),
2011            right: Box::new(RewriteCertificateV0::Rewrite {
2012                rule_id: "selector-is-single-v0".to_owned(),
2013                substitution: vec![substitution("x", ".a")],
2014                side_condition: SideConditionCertV0::NoSideCondition,
2015            }),
2016        };
2017        RewriteCertificateEnvelopeV0 {
2018            schema_version: REWRITE_CERTIFICATE_SCHEMA_VERSION_V0.to_owned(),
2019            max_depth: 5,
2020            max_nodes: 8,
2021            certificate: RewriteCertificateV0::Cong {
2022                operator: "selectorConcat".to_owned(),
2023                certificates: vec![
2024                    RewriteCertificateV0::Refl {
2025                        term: RewriteTermV0::atom(".root"),
2026                    },
2027                    inside_is,
2028                ],
2029            },
2030        }
2031    }
2032
2033    fn cascade_winner_key() -> CascadeWinnerKeyCertV0 {
2034        CascadeWinnerKeyCertV0 {
2035            level: CascadeLevelCertV0::AuthorNormal,
2036            layer_important: false,
2037            layer_ordinal: Some(0),
2038            scope_proximity: 0,
2039            specificity_ids: 0,
2040            specificity_classes: 2,
2041            specificity_elements: 0,
2042            source_order: 3,
2043        }
2044    }
2045
2046    fn cascade_winner_certificate() -> CascadeWinnerEqualityCertV0 {
2047        CascadeWinnerEqualityCertV0 {
2048            before_winner_id: "declaration-color".to_owned(),
2049            after_winner_id: "declaration-color".to_owned(),
2050            before_key: cascade_winner_key(),
2051            after_key: cascade_winner_key(),
2052            before_class_attribute: "root a".to_owned(),
2053            after_class_attribute: "root a".to_owned(),
2054        }
2055    }
2056
2057    fn selector_certificate_with_cascade_side_condition() -> RewriteCertificateEnvelopeV0 {
2058        let mut envelope = selector_certificate();
2059        replace_side_condition(
2060            &mut envelope.certificate,
2061            &SideConditionCertV0::CascadeWinnerEquality {
2062                certificate: cascade_winner_certificate(),
2063            },
2064        );
2065        envelope
2066    }
2067
2068    fn replace_side_condition(
2069        certificate: &mut RewriteCertificateV0,
2070        side_condition: &SideConditionCertV0,
2071    ) {
2072        match certificate {
2073            RewriteCertificateV0::Refl { .. } => {}
2074            RewriteCertificateV0::Sym { certificate } => {
2075                replace_side_condition(certificate, side_condition);
2076            }
2077            RewriteCertificateV0::Trans { left, right } => {
2078                replace_side_condition(left, side_condition);
2079                replace_side_condition(right, side_condition);
2080            }
2081            RewriteCertificateV0::Cong { certificates, .. } => {
2082                for child in certificates {
2083                    replace_side_condition(child, side_condition);
2084                }
2085            }
2086            RewriteCertificateV0::Rewrite {
2087                side_condition: observed,
2088                ..
2089            } => *observed = side_condition.clone(),
2090        }
2091    }
2092
2093    fn mutate_cascade_specificity(certificate: &mut RewriteCertificateV0) {
2094        match certificate {
2095            RewriteCertificateV0::Refl { .. } => {}
2096            RewriteCertificateV0::Sym { certificate } => {
2097                mutate_cascade_specificity(certificate);
2098            }
2099            RewriteCertificateV0::Trans { left, right } => {
2100                mutate_cascade_specificity(left);
2101                mutate_cascade_specificity(right);
2102            }
2103            RewriteCertificateV0::Cong { certificates, .. } => {
2104                for child in certificates {
2105                    mutate_cascade_specificity(child);
2106                }
2107            }
2108            RewriteCertificateV0::Rewrite {
2109                side_condition: SideConditionCertV0::CascadeWinnerEquality { certificate },
2110                ..
2111            } => {
2112                certificate.after_key.specificity_classes =
2113                    certificate.after_key.specificity_classes.saturating_add(1);
2114            }
2115            RewriteCertificateV0::Rewrite { .. } => {}
2116        }
2117    }
2118
2119    fn single_rule_catalog(
2120        rule_id: &str,
2121        before: &str,
2122        after: &str,
2123        side_condition_kind: RewriteSideConditionKindV0,
2124    ) -> RewriteRuleCatalogV0 {
2125        RewriteRuleCatalogV0 {
2126            schema_version: REWRITE_RULE_CATALOG_SCHEMA_VERSION_V0.to_owned(),
2127            operators: Vec::new(),
2128            rules: vec![RewriteRuleV0 {
2129                rule_id: rule_id.to_owned(),
2130                before_pattern: RewritePatternV0::atom(before),
2131                after_pattern: RewritePatternV0::atom(after),
2132                side_condition_kind,
2133            }],
2134        }
2135    }
2136
2137    fn single_rule_certificate(
2138        rule_id: &str,
2139        side_condition: SideConditionCertV0,
2140    ) -> RewriteCertificateEnvelopeV0 {
2141        RewriteCertificateEnvelopeV0 {
2142            schema_version: REWRITE_CERTIFICATE_SCHEMA_VERSION_V0.to_owned(),
2143            max_depth: 1,
2144            max_nodes: 1,
2145            certificate: RewriteCertificateV0::Rewrite {
2146                rule_id: rule_id.to_owned(),
2147                substitution: Vec::new(),
2148                side_condition,
2149            },
2150        }
2151    }
2152
2153    fn literal(value: &str) -> ComputedValueTermV0 {
2154        ComputedValueTermV0::Literal {
2155            value: value.to_owned(),
2156        }
2157    }
2158
2159    fn computed_value_certificate(after_value: &str) -> ComputedValueEqualityCertV0 {
2160        ComputedValueEqualityCertV0 {
2161            property: "--space".to_owned(),
2162            before_environment: vec![
2163                ComputedValueEnvironmentEntryV0 {
2164                    name: "--base".to_owned(),
2165                    value: literal("8px"),
2166                },
2167                ComputedValueEnvironmentEntryV0 {
2168                    name: "--space".to_owned(),
2169                    value: ComputedValueTermV0::Variable {
2170                        name: "--base".to_owned(),
2171                        fallback: None,
2172                    },
2173                },
2174            ],
2175            after_environment: vec![
2176                ComputedValueEnvironmentEntryV0 {
2177                    name: "--base".to_owned(),
2178                    value: literal("8px"),
2179                },
2180                ComputedValueEnvironmentEntryV0 {
2181                    name: "--space".to_owned(),
2182                    value: literal(after_value),
2183                },
2184            ],
2185        }
2186    }
2187
2188    fn source_map_certificate() -> SourceMapTraceCertV0 {
2189        SourceMapTraceCertV0 {
2190            before_segments: vec![SourceMapTraceSegmentV0 {
2191                source_path: "fixture/selector.module.css".to_owned(),
2192                source_digest: "c6f1172b".to_owned(),
2193                original_start: 0,
2194                original_end: 15,
2195                generated_start: 0,
2196                generated_end: 15,
2197                pass_id: "selector-is-where-compression".to_owned(),
2198            }],
2199            after_segments: vec![SourceMapTraceSegmentV0 {
2200                source_path: "fixture/selector.module.css".to_owned(),
2201                source_digest: "c6f1172b".to_owned(),
2202                original_start: 0,
2203                original_end: 15,
2204                generated_start: 0,
2205                generated_end: 10,
2206                pass_id: "selector-is-where-compression".to_owned(),
2207            }],
2208        }
2209    }
2210
2211    fn check_selector(
2212        catalog: &RewriteRuleCatalogV0,
2213        certificate: &RewriteCertificateEnvelopeV0,
2214    ) -> Result<RewriteIssuanceTokenV0, CertificateRejectionV0> {
2215        let (before, after) = selector_terms();
2216        check_rewrite_certificate_v0(
2217            &before,
2218            &after,
2219            catalog,
2220            certificate,
2221            &CanonicalRewriteAssumptionsV0::default(),
2222        )
2223    }
2224
2225    #[test]
2226    fn real_selector_rewrite_accepts_cascade_winner_equality_certificate() {
2227        let result = check_selector(
2228            &selector_rewrite_rule_catalog_with_cascade_winner_equality_v0(),
2229            &selector_certificate_with_cascade_side_condition(),
2230        );
2231        assert!(result.is_ok(), "cascade cert rejected: {result:?}");
2232        let Ok(token) = result else {
2233            return;
2234        };
2235        println!(
2236            "cascadeWinnerEquality=accepted checkedRules={:?}",
2237            token.checked_rule_ids_v0()
2238        );
2239    }
2240
2241    #[test]
2242    fn specificity_perturbation_rejects_cascade_cert_without_producer_boolean_input() {
2243        let catalog = selector_rewrite_rule_catalog_with_cascade_winner_equality_v0();
2244        let mut certificate = selector_certificate_with_cascade_side_condition();
2245        mutate_cascade_specificity(&mut certificate.certificate);
2246        let producer_specificity_preserved_values = [true, false];
2247        let mut rejections = Vec::new();
2248        for producer_specificity_preserved in producer_specificity_preserved_values {
2249            let result = check_selector(&catalog, &certificate);
2250            assert!(
2251                result.is_err(),
2252                "specificity perturbation accepted with producer={producer_specificity_preserved}"
2253            );
2254            let Err(rejection) = result else {
2255                continue;
2256            };
2257            rejections.push((*rejection.rejection).clone());
2258        }
2259        assert_eq!(rejections.len(), 2);
2260        assert_eq!(rejections[0], rejections[1]);
2261        assert!(matches!(
2262            rejections[0],
2263            CertificateRejectionKindV0::CascadeWinnerEqualityRejected {
2264                cascade_keys_equal: false,
2265                ..
2266            }
2267        ));
2268        println!(
2269            "specificityPerturbation={:?} producerFieldTrueFalseSame={}",
2270            rejections[0],
2271            rejections[0] == rejections[1]
2272        );
2273    }
2274
2275    #[test]
2276    fn custom_property_rewrite_accepts_and_rejects_from_fixed_point_values() {
2277        let catalog = single_rule_catalog(
2278            "custom-property-inline-v0",
2279            "var(--base)",
2280            "8px",
2281            RewriteSideConditionKindV0::ComputedValueEquality,
2282        );
2283        let before = RewriteTermV0::atom("var(--base)");
2284        let after = RewriteTermV0::atom("8px");
2285        let accepted = single_rule_certificate(
2286            "custom-property-inline-v0",
2287            SideConditionCertV0::ComputedValueEquality {
2288                certificate: computed_value_certificate("8px"),
2289            },
2290        );
2291        let accepted_result = check_rewrite_certificate_v0(
2292            &before,
2293            &after,
2294            &catalog,
2295            &accepted,
2296            &CanonicalRewriteAssumptionsV0::default(),
2297        );
2298        assert!(
2299            accepted_result.is_ok(),
2300            "computed-value cert rejected: {accepted_result:?}"
2301        );
2302
2303        let rejected = single_rule_certificate(
2304            "custom-property-inline-v0",
2305            SideConditionCertV0::ComputedValueEquality {
2306                certificate: computed_value_certificate("9px"),
2307            },
2308        );
2309        let rejected_result = check_rewrite_certificate_v0(
2310            &before,
2311            &after,
2312            &catalog,
2313            &rejected,
2314            &CanonicalRewriteAssumptionsV0::default(),
2315        );
2316        assert!(
2317            rejected_result.is_err(),
2318            "computed-value perturbation accepted"
2319        );
2320        let Err(rejection) = rejected_result else {
2321            return;
2322        };
2323        assert!(matches!(
2324            *rejection.rejection,
2325            CertificateRejectionKindV0::ComputedValueEqualityRejected { .. }
2326        ));
2327        println!(
2328            "computedValue accepted=true perturbedRejection={:?}",
2329            rejection.rejection
2330        );
2331    }
2332
2333    #[test]
2334    fn selector_rewrite_accepts_and_rejects_from_emitted_source_map_segments() {
2335        let catalog = single_rule_catalog(
2336            "selector-trace-v0",
2337            ".root:is(.a)",
2338            ".root.a",
2339            RewriteSideConditionKindV0::SourceMapTrace,
2340        );
2341        let before = RewriteTermV0::atom(".root:is(.a)");
2342        let after = RewriteTermV0::atom(".root.a");
2343        let accepted = single_rule_certificate(
2344            "selector-trace-v0",
2345            SideConditionCertV0::SourceMapTrace {
2346                certificate: source_map_certificate(),
2347            },
2348        );
2349        let accepted_result = check_rewrite_certificate_v0(
2350            &before,
2351            &after,
2352            &catalog,
2353            &accepted,
2354            &CanonicalRewriteAssumptionsV0::default(),
2355        );
2356        assert!(
2357            accepted_result.is_ok(),
2358            "source-map cert rejected: {accepted_result:?}"
2359        );
2360
2361        let mut perturbed_trace = source_map_certificate();
2362        perturbed_trace.after_segments[0].original_start = 1;
2363        let rejected = single_rule_certificate(
2364            "selector-trace-v0",
2365            SideConditionCertV0::SourceMapTrace {
2366                certificate: perturbed_trace,
2367            },
2368        );
2369        let rejected_result = check_rewrite_certificate_v0(
2370            &before,
2371            &after,
2372            &catalog,
2373            &rejected,
2374            &CanonicalRewriteAssumptionsV0::default(),
2375        );
2376        assert!(rejected_result.is_err(), "source-map perturbation accepted");
2377        let Err(rejection) = rejected_result else {
2378            return;
2379        };
2380        assert!(matches!(
2381            *rejection.rejection,
2382            CertificateRejectionKindV0::SourceMapTraceRejected {
2383                segment_index: 0,
2384                ..
2385            }
2386        ));
2387        println!(
2388            "sourceMapTrace accepted=true perturbedRejection={:?}",
2389            rejection.rejection
2390        );
2391    }
2392
2393    #[test]
2394    fn side_condition_certificates_round_trip_through_serde() -> Result<(), serde_json::Error> {
2395        let certificates = [
2396            SideConditionCertV0::CascadeWinnerEquality {
2397                certificate: cascade_winner_certificate(),
2398            },
2399            SideConditionCertV0::ComputedValueEquality {
2400                certificate: computed_value_certificate("8px"),
2401            },
2402            SideConditionCertV0::SourceMapTrace {
2403                certificate: source_map_certificate(),
2404            },
2405            SideConditionCertV0::TokenOwnershipSeparability {
2406                certificate: TokenOwnershipSeparabilityCertV0 {
2407                    complete: true,
2408                    modeled_preimage_count: 1,
2409                    emitted_token_count: 1,
2410                    ownerships: vec![TokenOwnershipCertEntryV0 {
2411                        emitted_token: "_shared_0".to_owned(),
2412                        module_paths: vec!["src/one.module.css".to_owned()],
2413                    }],
2414                    unattributed_emitted_token_count: 0,
2415                    interface_mismatch_count: 0,
2416                },
2417            },
2418            SideConditionCertV0::TransformIndependence {
2419                certificate: Box::new(TransformIndependenceCertV0 {
2420                    left_pass_id: "number-compression".to_owned(),
2421                    right_pass_id: "color-compression".to_owned(),
2422                    observation_profile_id: "exact-emission-bytes-v0".to_owned(),
2423                    profile_observers: vec!["rawBytes".to_owned()],
2424                    observation_rows: vec![TransformIndependenceObservationCertRowV0 {
2425                        fixture_id: "disjoint-values".to_owned(),
2426                        observer: "rawBytes".to_owned(),
2427                        left_then_right: ".a{color:red;margin:.5px}".to_owned(),
2428                        right_then_left: ".a{color:red;margin:.5px}".to_owned(),
2429                    }],
2430                    left_preconditions: vec!["equivalentLiteralValue".to_owned()],
2431                    right_preconditions: vec!["equivalentLiteralValue".to_owned()],
2432                    left_preserves_right_preconditions: vec!["equivalentLiteralValue".to_owned()],
2433                    right_preserves_left_preconditions: vec!["equivalentLiteralValue".to_owned()],
2434                    disqualifying_descriptor_edges: Vec::new(),
2435                }),
2436            },
2437        ];
2438        for certificate in certificates {
2439            let encoded = serde_json::to_string(&certificate)?;
2440            let decoded = serde_json::from_str::<SideConditionCertV0>(&encoded)?;
2441            assert_eq!(decoded, certificate);
2442        }
2443        Ok(())
2444    }
2445
2446    #[test]
2447    fn token_ownership_side_condition_requires_one_owner_per_emitted_token() {
2448        let catalog = single_rule_catalog(
2449            "closed-world-ownership-admission-v0",
2450            "admissionRequested",
2451            "admissionGranted",
2452            RewriteSideConditionKindV0::TokenOwnershipSeparability,
2453        );
2454        let before = RewriteTermV0::atom("admissionRequested");
2455        let after = RewriteTermV0::atom("admissionGranted");
2456        let certificate = |module_paths: Vec<String>, modeled_preimage_count| {
2457            single_rule_certificate(
2458                "closed-world-ownership-admission-v0",
2459                SideConditionCertV0::TokenOwnershipSeparability {
2460                    certificate: TokenOwnershipSeparabilityCertV0 {
2461                        complete: true,
2462                        modeled_preimage_count,
2463                        emitted_token_count: 1,
2464                        ownerships: vec![TokenOwnershipCertEntryV0 {
2465                            emitted_token: "_shared_0".to_owned(),
2466                            module_paths,
2467                        }],
2468                        unattributed_emitted_token_count: 0,
2469                        interface_mismatch_count: 0,
2470                    },
2471                },
2472            )
2473        };
2474        let accepted = check_rewrite_certificate_v0(
2475            &before,
2476            &after,
2477            &catalog,
2478            &certificate(vec!["src/one.module.css".to_owned()], 1),
2479            &CanonicalRewriteAssumptionsV0::default(),
2480        );
2481        assert!(accepted.is_ok(), "unique ownership rejected: {accepted:?}");
2482
2483        let rejected = check_rewrite_certificate_v0(
2484            &before,
2485            &after,
2486            &catalog,
2487            &certificate(
2488                vec![
2489                    "src/one.module.css".to_owned(),
2490                    "src/two.module.css".to_owned(),
2491                ],
2492                2,
2493            ),
2494            &CanonicalRewriteAssumptionsV0::default(),
2495        );
2496        assert!(rejected.is_err(), "ambiguous ownership accepted");
2497        let Err(rejection) = rejected else {
2498            return;
2499        };
2500        assert!(matches!(
2501            *rejection.rejection,
2502            CertificateRejectionKindV0::TokenOwnershipSeparabilityRejected {
2503                token: Some(ref token),
2504                ..
2505            } if token == "_shared_0"
2506        ));
2507    }
2508
2509    #[test]
2510    fn transform_independence_requires_observation_and_precondition_halves() {
2511        let catalog = single_rule_catalog(
2512            "adjacent-schedule-swap-v0",
2513            "numberThenColor",
2514            "colorThenNumber",
2515            RewriteSideConditionKindV0::TransformIndependence,
2516        );
2517        let before = RewriteTermV0::atom("numberThenColor");
2518        let after = RewriteTermV0::atom("colorThenNumber");
2519        let independence = TransformIndependenceCertV0 {
2520            left_pass_id: "number-compression".to_owned(),
2521            right_pass_id: "color-compression".to_owned(),
2522            observation_profile_id: "exact-emission-bytes-v0".to_owned(),
2523            profile_observers: vec!["rawBytes".to_owned()],
2524            observation_rows: vec![TransformIndependenceObservationCertRowV0 {
2525                fixture_id: "disjoint-values".to_owned(),
2526                observer: "rawBytes".to_owned(),
2527                left_then_right: ".a{color:red;margin:.5px}".to_owned(),
2528                right_then_left: ".a{color:red;margin:.5px}".to_owned(),
2529            }],
2530            left_preconditions: vec!["equivalentLiteralValue".to_owned()],
2531            right_preconditions: vec!["equivalentLiteralValue".to_owned()],
2532            left_preserves_right_preconditions: vec!["equivalentLiteralValue".to_owned()],
2533            right_preserves_left_preconditions: vec!["equivalentLiteralValue".to_owned()],
2534            disqualifying_descriptor_edges: Vec::new(),
2535        };
2536        let envelope = |certificate| {
2537            single_rule_certificate(
2538                "adjacent-schedule-swap-v0",
2539                SideConditionCertV0::TransformIndependence {
2540                    certificate: Box::new(certificate),
2541                },
2542            )
2543        };
2544        let accepted = check_rewrite_certificate_v0(
2545            &before,
2546            &after,
2547            &catalog,
2548            &envelope(independence.clone()),
2549            &CanonicalRewriteAssumptionsV0::default(),
2550        );
2551        assert!(accepted.is_ok(), "independence cert rejected: {accepted:?}");
2552
2553        let mut dependent = independence;
2554        dependent
2555            .disqualifying_descriptor_edges
2556            .push("conflictsWith:color-mix-lowering:color-function-lowering".to_owned());
2557        let rejected = check_rewrite_certificate_v0(
2558            &before,
2559            &after,
2560            &catalog,
2561            &envelope(dependent),
2562            &CanonicalRewriteAssumptionsV0::default(),
2563        );
2564        assert!(matches!(
2565            rejected,
2566            Err(CertificateRejectionV0 {
2567                rejection,
2568                ..
2569            }) if matches!(
2570                *rejection,
2571                CertificateRejectionKindV0::TransformIndependenceRejected { .. }
2572            )
2573        ));
2574    }
2575
2576    #[test]
2577    fn real_selector_trans_cong_rewrite_chain_issues_token() {
2578        let catalog = selector_rewrite_rule_catalog_v0();
2579        let result = check_selector(&catalog, &selector_certificate());
2580        assert!(result.is_ok(), "selector certificate rejected: {result:?}");
2581        let Ok(token) = result else {
2582            return;
2583        };
2584
2585        assert_eq!(
2586            token.checked_rule_ids_v0(),
2587            ["selector-list-deduplicate-v0", "selector-is-single-v0"]
2588        );
2589        assert_ne!(token.before_digest_hex_v0(), token.after_digest_hex_v0());
2590        assert_eq!(
2591            token.catalog_schema_id_v0(),
2592            REWRITE_RULE_CATALOG_SCHEMA_ID_V0
2593        );
2594        assert!(token.matches_catalog_v0(&catalog));
2595        println!(
2596            "issued=true beforeDigest={} afterDigest={} catalogSchemaId={} catalogDigest={} checkedRules={:?}",
2597            token.before_digest_hex_v0(),
2598            token.after_digest_hex_v0(),
2599            token.catalog_schema_id_v0(),
2600            token.catalog_content_digest_hex_v0(),
2601            token.checked_rule_ids_v0()
2602        );
2603    }
2604
2605    #[test]
2606    fn catalog_digest_is_order_independent_and_content_sensitive() {
2607        let catalog = selector_rewrite_rule_catalog_v0();
2608        let mut permuted = catalog.clone();
2609        permuted.operators.reverse();
2610        permuted.rules.reverse();
2611        assert_eq!(
2612            rewrite_rule_catalog_content_digest_hex_v0(&catalog),
2613            rewrite_rule_catalog_content_digest_hex_v0(&permuted)
2614        );
2615
2616        let mut spoofed = catalog.clone();
2617        spoofed.rules[1].before_pattern = RewritePatternV0::variable("anything");
2618        spoofed.rules[1].after_pattern = RewritePatternV0::variable("whatever");
2619        assert_ne!(
2620            rewrite_rule_catalog_content_digest_hex_v0(&catalog),
2621            rewrite_rule_catalog_content_digest_hex_v0(&spoofed)
2622        );
2623    }
2624
2625    #[test]
2626    fn one_token_substitution_mutation_names_the_after_subterm() {
2627        let mut certificate = selector_certificate();
2628        let RewriteCertificateV0::Cong { certificates, .. } = &mut certificate.certificate else {
2629            unreachable!("fixture root is congruence")
2630        };
2631        let RewriteCertificateV0::Trans { right, .. } = &mut certificates[1] else {
2632            unreachable!("fixture inner node is transitivity")
2633        };
2634        let RewriteCertificateV0::Rewrite { substitution, .. } = right.as_mut() else {
2635            unreachable!("fixture right node is rewrite")
2636        };
2637        substitution[0].term = RewriteTermV0::atom(".b");
2638
2639        let result = check_selector(&selector_rewrite_rule_catalog_v0(), &certificate);
2640        assert!(result.is_err(), "one-token substitution mutation accepted");
2641        let Err(rejection) = result else {
2642            return;
2643        };
2644        assert_eq!(
2645            *rejection.rejection,
2646            CertificateRejectionKindV0::TransitiveMiddleMismatch
2647        );
2648        assert_eq!(rejection.site.certificate_path, vec![1]);
2649        assert_eq!(rejection.site.term_path, vec![0]);
2650        println!(
2651            "rejection={:?} certificatePath={:?} termPath={:?}",
2652            rejection.rejection, rejection.site.certificate_path, rejection.site.term_path
2653        );
2654    }
2655
2656    #[test]
2657    fn favourable_producer_fields_cannot_replace_a_missing_certificate() {
2658        let producer_specificity_preserved = true;
2659        let producer_computed_value_preserved = true;
2660        let producer_provenance_preserved = true;
2661        assert!(
2662            producer_specificity_preserved
2663                && producer_computed_value_preserved
2664                && producer_provenance_preserved
2665        );
2666        let (before, after) = selector_terms();
2667        let result = check_optional_rewrite_certificate_v0(
2668            &before,
2669            &after,
2670            &selector_rewrite_rule_catalog_v0(),
2671            None,
2672            &CanonicalRewriteAssumptionsV0::default(),
2673        );
2674        assert!(result.is_err(), "missing certificate minted a token");
2675        let Err(rejection) = result else {
2676            return;
2677        };
2678        assert_eq!(
2679            *rejection.rejection,
2680            CertificateRejectionKindV0::MissingCertificate
2681        );
2682        println!(
2683            "producerFields=true,true,true rejection={:?}",
2684            rejection.rejection
2685        );
2686    }
2687
2688    #[test]
2689    fn adversarial_corpus_returns_six_typed_rejections_without_panicking() {
2690        let catalog = selector_rewrite_rule_catalog_v0();
2691        let (before, after) = selector_terms();
2692
2693        let mut unknown_rule = selector_certificate();
2694        let RewriteCertificateV0::Cong { certificates, .. } = &mut unknown_rule.certificate else {
2695            unreachable!("fixture root is congruence")
2696        };
2697        let RewriteCertificateV0::Trans { right, .. } = &mut certificates[1] else {
2698            unreachable!("fixture inner node is transitivity")
2699        };
2700        let RewriteCertificateV0::Rewrite { rule_id, .. } = right.as_mut() else {
2701            unreachable!("fixture right node is rewrite")
2702        };
2703        *rule_id = "unknown-rule-v0".to_owned();
2704
2705        let mut arity = selector_certificate();
2706        let RewriteCertificateV0::Cong { certificates, .. } = &mut arity.certificate else {
2707            unreachable!("fixture root is congruence")
2708        };
2709        certificates.pop();
2710
2711        let missing_substitution = RewriteCertificateEnvelopeV0 {
2712            schema_version: REWRITE_CERTIFICATE_SCHEMA_VERSION_V0.to_owned(),
2713            max_depth: 1,
2714            max_nodes: 1,
2715            certificate: RewriteCertificateV0::Rewrite {
2716                rule_id: "selector-is-single-v0".to_owned(),
2717                substitution: Vec::new(),
2718                side_condition: SideConditionCertV0::NoSideCondition,
2719            },
2720        };
2721
2722        let mut depth = selector_certificate();
2723        depth.max_depth = 2;
2724
2725        let trans_mismatch = RewriteCertificateEnvelopeV0 {
2726            schema_version: REWRITE_CERTIFICATE_SCHEMA_VERSION_V0.to_owned(),
2727            max_depth: 2,
2728            max_nodes: 3,
2729            certificate: RewriteCertificateV0::Trans {
2730                left: Box::new(RewriteCertificateV0::Refl {
2731                    term: RewriteTermV0::atom(".a"),
2732                }),
2733                right: Box::new(RewriteCertificateV0::Refl {
2734                    term: RewriteTermV0::atom(".b"),
2735                }),
2736            },
2737        };
2738
2739        let typed_cases = [
2740            ("unknownRule", unknown_rule, "unknownRule"),
2741            ("congruenceArity", arity, "operatorArityMismatch"),
2742            (
2743                "missingSubstitution",
2744                missing_substitution,
2745                "missingSubstitutionVariable",
2746            ),
2747            ("declaredDepth", depth, "declaredBoundExceeded"),
2748            (
2749                "transitiveMiddle",
2750                trans_mismatch,
2751                "transitiveMiddleMismatch",
2752            ),
2753        ];
2754        let mut observed = Vec::new();
2755        for (name, certificate, expected_kind) in typed_cases {
2756            let outcome = catch_unwind(AssertUnwindSafe(|| {
2757                check_rewrite_certificate_v0(
2758                    &before,
2759                    &after,
2760                    &catalog,
2761                    &certificate,
2762                    &CanonicalRewriteAssumptionsV0::default(),
2763                )
2764            }));
2765            assert!(outcome.is_ok(), "{name} panicked");
2766            let Some(result) = outcome.ok() else {
2767                continue;
2768            };
2769            assert!(result.is_err(), "{name} did not produce a typed rejection");
2770            let Err(rejection) = result else {
2771                continue;
2772            };
2773            let encoded = serde_json::to_value(&rejection);
2774            assert!(encoded.is_ok(), "{name} rejection did not serialize");
2775            let Ok(value) = encoded else {
2776                continue;
2777            };
2778            assert_eq!(value["rejection"]["kind"], expected_kind);
2779            assert_eq!(value["site"]["input"], "certificate");
2780            println!(
2781                "case={name} rejection={} site={}",
2782                value["rejection"], value["site"]
2783            );
2784            observed.push(name);
2785        }
2786
2787        let malformed = catch_unwind(AssertUnwindSafe(|| {
2788            check_serialized_rewrite_certificate_v0(
2789                &before,
2790                &after,
2791                &catalog,
2792                r#"{"schemaVersion":"0","certificate":{"kind":"trans""#,
2793                &CanonicalRewriteAssumptionsV0::default(),
2794            )
2795        }));
2796        assert!(malformed.is_ok(), "malformed serde input panicked");
2797        let Some(result) = malformed.ok() else {
2798            return;
2799        };
2800        assert!(result.is_err(), "malformed serde input was not rejected");
2801        let Err(rejection) = result else {
2802            return;
2803        };
2804        assert!(matches!(
2805            *rejection.rejection,
2806            CertificateRejectionKindV0::MalformedCertificate { .. }
2807        ));
2808        assert_eq!(
2809            rejection.site.input,
2810            RewriteCheckInputV0::SerializedCertificate
2811        );
2812        println!(
2813            "case=malformedSerde rejection={:?} site={:?}",
2814            rejection.rejection, rejection.site
2815        );
2816        observed.push("malformedSerde");
2817        assert_eq!(observed.len(), 6);
2818    }
2819
2820    #[test]
2821    fn fixed_seed_input_order_permutations_issue_identical_tokens() {
2822        let baseline_catalog = selector_rewrite_rule_catalog_v0();
2823        let baseline_certificate = selector_certificate();
2824        let baseline_result = check_selector(&baseline_catalog, &baseline_certificate);
2825        assert!(
2826            baseline_result.is_ok(),
2827            "baseline rejected: {baseline_result:?}"
2828        );
2829        let Ok(baseline) = baseline_result else {
2830            return;
2831        };
2832        let mut state = 0x6a09_e667_f3bc_c909_u64;
2833
2834        for _ in 0..32 {
2835            let mut catalog = selector_rewrite_rule_catalog_v0();
2836            seeded_shuffle(&mut catalog.operators, &mut state);
2837            seeded_shuffle(&mut catalog.rules, &mut state);
2838            let mut certificate = selector_certificate();
2839            reverse_substitution_order(&mut certificate.certificate);
2840            let observed_result = check_selector(&catalog, &certificate);
2841            assert!(
2842                observed_result.is_ok(),
2843                "permutation rejected: {observed_result:?}"
2844            );
2845            let Ok(observed) = observed_result else {
2846                continue;
2847            };
2848            assert_eq!(observed, baseline);
2849        }
2850    }
2851
2852    fn reverse_substitution_order(certificate: &mut RewriteCertificateV0) {
2853        match certificate {
2854            RewriteCertificateV0::Refl { .. } => {}
2855            RewriteCertificateV0::Sym { certificate } => {
2856                reverse_substitution_order(certificate);
2857            }
2858            RewriteCertificateV0::Trans { left, right } => {
2859                reverse_substitution_order(left);
2860                reverse_substitution_order(right);
2861            }
2862            RewriteCertificateV0::Cong { certificates, .. } => {
2863                for child in certificates {
2864                    reverse_substitution_order(child);
2865                }
2866            }
2867            RewriteCertificateV0::Rewrite { substitution, .. } => substitution.reverse(),
2868        }
2869    }
2870
2871    fn seeded_shuffle<T>(values: &mut [T], state: &mut u64) {
2872        for index in (1..values.len()).rev() {
2873            *state = state
2874                .wrapping_mul(6_364_136_223_846_793_005)
2875                .wrapping_add(1_442_695_040_888_963_407);
2876            let target = (*state as usize) % (index + 1);
2877            values.swap(index, target);
2878        }
2879    }
2880
2881    #[test]
2882    fn grammar_round_trips_through_serde() -> Result<(), serde_json::Error> {
2883        let certificate = selector_certificate();
2884        let encoded = serde_json::to_string(&certificate)?;
2885        let decoded = serde_json::from_str::<RewriteCertificateEnvelopeV0>(&encoded)?;
2886        assert_eq!(decoded, certificate);
2887        Ok(())
2888    }
2889}