Skip to main content

omena_transform_cst/
lib.rs

1//! Transform CST contract substrate for the post-v5 omena-css track.
2//!
3//! This crate intentionally starts at the contract layer: transform passes are
4//! only valid when they declare which semantic/cascade facts they read and what
5//! cascade-safety obligation they must preserve.
6
7use omena_cascade_proof::{
8    CascadeSMTProofV0, SmtBackendV0, SmtVerdictV0, StubSmtBackendV0, TransformRewriteProofInputV0,
9    smt_verify_transform_rewrite_candidate_v0,
10};
11use omena_evidence_graph::{
12    EvidenceDemandEdgeV0, EvidenceGraphBuildErrorV0, EvidenceGraphV0, EvidenceNodeKeyV0,
13    EvidenceNodeSeedV0, FamilyStampV0, GuaranteeKindV0, ObligationFamilyIdV0,
14    ProseObligationProvenanceV0, build_evidence_graph_from_edges_v0,
15};
16pub use omena_parser::StyleDialect;
17use omena_parser::{
18    ClosedWorldBundleV0, ParsedAnimationFactKind, ParsedCssModuleComposesFactKind,
19    ParsedCssModuleValueFactKind, ParsedIcssFactKind, ParsedSassSymbolFactKind,
20    ParsedSelectorFactKind, ParsedVariableFactKind, collect_style_facts,
21};
22use serde::{Serialize, ser::SerializeStruct};
23use std::{borrow::Cow, collections::BTreeMap, sync::OnceLock};
24
25mod pass_descriptor;
26mod transform_ir;
27pub use pass_descriptor::{
28    MinifyPassClassificationDerivationV0, MinifyPassClassificationV0, MinifyPassProfileClassV0,
29    ObservationKindV0, PassAssumptionKindV0, PassObservationSurfaceV0, PassSemanticContractV0,
30    STRICT_VERIFICATION_BUILD_PROFILE_ID_V0, TransformBuildProfileV0, TransformPassClassV0,
31    TransformPassDescriptorV0, TransformPassObservationRecordV0, TransformStrictPolicyDescriptorV0,
32    closed_world_minify_build_profile, default_minify_build_profiles,
33    default_minify_pass_classifications, default_transform_pass_descriptors,
34    default_transform_pass_observation_records, minify_pass_profile_classification,
35    pass_observation_contract, safe_minify_build_profile, semantic_minify_build_profile,
36    strict_policy_descriptor_for_profile, strict_verification_build_profile,
37    strict_verification_policy_descriptor, transform_build_profile_from_passes,
38    transform_pass_class, transform_pass_descriptor, transform_pass_requires_closed_world_bundle,
39};
40pub use transform_ir::{
41    IrBlockSpanV0, IrEditRegionV0, IrNodeIdV0, IrNodeKindV0, IrNodeV0, IrTargetV0,
42    IrTransactionErrorV0, IrTransactionV0, IrTransactionValidationErrorV0, NodeTextOriginV0,
43    TransformIrIdentityRoundTripV0, TransformIrIndexesV0, TransformIrKindIndexV0,
44    TransformIrParentIndexV0, TransformIrParseErrorSpanV0, TransformIrPrintErrorV0, TransformIrV0,
45    lower_transform_ir_from_source, materialize_transform_ir_printed_source,
46    print_transform_ir_css, structural_block_spans_for_source,
47    summarize_transform_ir_identity_round_trip,
48};
49
50use std::cell::RefCell;
51
52const CASCADE_WITNESS_EVIDENCE_QUERY_V0: &str = "omena-transform-cst.cascade-safety-witness";
53const CASCADE_WITNESS_EVIDENCE_EDGE_KIND_V0: &str = "cascade-safety-evidence";
54
55fn prose_obligation_family_stamp(provenance: &[String]) -> FamilyStampV0 {
56    let Some(prose_provenance) = ProseObligationProvenanceV0::from_provenance_labels(provenance)
57    else {
58        unreachable!("prose evidence seeds include an obligation provenance label")
59    };
60    FamilyStampV0::prose_obligation_discharged(&prose_provenance)
61}
62pub const STABLE_NODE_KEY_STRING_ARM_EXPIRY_UTC_DATE_V0: &str = "2026-10-01";
63pub const STABLE_NODE_KEY_TYPE_LABEL_V0: &str = "StableNodeKeyV0";
64
65#[cfg(stable_node_key_string_arm_expired)]
66compile_error!(
67    "StableNodeKeyV0 string arm has passed its expiry date; migrate consumers to StableNodeKeyU64V0 or extend the expiry with a tracked decision."
68);
69
70#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Serialize)]
71#[serde(rename_all = "camelCase")]
72pub enum TransformLayer {
73    SemanticReadOnly,
74    SemanticAware,
75    Commodity,
76    Emission,
77}
78
79#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Serialize)]
80#[serde(rename_all = "camelCase")]
81pub enum TransformPassKind {
82    WhitespaceStrip,
83    CommentStrip,
84    NumberCompression,
85    UnitNormalization,
86    ColorCompression,
87    UrlQuoteStrip,
88    StringQuoteNormalize,
89    SelectorIsWhereCompression,
90    ShorthandCombining,
91    RuleDeduplication,
92    RuleMerging,
93    SelectorMerging,
94    EmptyRuleRemoval,
95    VendorPrefixing,
96    StalePrefixRemoval,
97    LightDarkLowering,
98    ColorMixLowering,
99    OklchOklabLowering,
100    ColorFunctionLowering,
101    RelativeColorLowering,
102    LogicalToPhysical,
103    NestingUnwrap,
104    ScopeFlatten,
105    LayerFlatten,
106    SupportsStaticEval,
107    MediaStaticEval,
108    ContainerStaticEval,
109    NativeCssStaticEval,
110    CalcReduction,
111    ImportInline,
112    ScssModuleEvaluate,
113    LessModuleEvaluate,
114    HashCssModuleClassNames,
115    ResolveCssModulesComposes,
116    ValueResolution,
117    StaticVarSubstitution,
118    TreeShakeClass,
119    TreeShakeKeyframes,
120    TreeShakeValue,
121    TreeShakeCustomProperty,
122    DeadMediaBranchRemoval,
123    DeadSupportsBranchRemoval,
124    DesignTokenRouting,
125    PrintCss,
126}
127
128pub const TRANSFORM_PASS_CATALOG_LEN: usize = 44;
129pub const NATIVE_CSS_STATIC_EVAL_SPEC_SNAPSHOT_V0: &str =
130    "css-values-5-if-css-mixins-1-function-ed-2026-06-22";
131pub const NATIVE_CSS_STATIC_EVAL_OPT_IN_POLICY_V0: &str =
132    "explicit-pass-id-required-default-consumer-build-excludes";
133pub const NATIVE_CSS_STATIC_EVAL_DIALECT_RESTRICTION_V0: &str = "css-only";
134
135pub const fn all_transform_pass_kinds() -> [TransformPassKind; TRANSFORM_PASS_CATALOG_LEN] {
136    [
137        TransformPassKind::WhitespaceStrip,
138        TransformPassKind::CommentStrip,
139        TransformPassKind::NumberCompression,
140        TransformPassKind::UnitNormalization,
141        TransformPassKind::ColorCompression,
142        TransformPassKind::UrlQuoteStrip,
143        TransformPassKind::StringQuoteNormalize,
144        TransformPassKind::SelectorIsWhereCompression,
145        TransformPassKind::ShorthandCombining,
146        TransformPassKind::RuleDeduplication,
147        TransformPassKind::RuleMerging,
148        TransformPassKind::SelectorMerging,
149        TransformPassKind::EmptyRuleRemoval,
150        TransformPassKind::VendorPrefixing,
151        TransformPassKind::StalePrefixRemoval,
152        TransformPassKind::LightDarkLowering,
153        TransformPassKind::ColorMixLowering,
154        TransformPassKind::OklchOklabLowering,
155        TransformPassKind::ColorFunctionLowering,
156        TransformPassKind::RelativeColorLowering,
157        TransformPassKind::LogicalToPhysical,
158        TransformPassKind::NestingUnwrap,
159        TransformPassKind::ScopeFlatten,
160        TransformPassKind::LayerFlatten,
161        TransformPassKind::SupportsStaticEval,
162        TransformPassKind::MediaStaticEval,
163        TransformPassKind::ContainerStaticEval,
164        TransformPassKind::NativeCssStaticEval,
165        TransformPassKind::CalcReduction,
166        TransformPassKind::ImportInline,
167        TransformPassKind::ScssModuleEvaluate,
168        TransformPassKind::LessModuleEvaluate,
169        TransformPassKind::HashCssModuleClassNames,
170        TransformPassKind::ResolveCssModulesComposes,
171        TransformPassKind::ValueResolution,
172        TransformPassKind::StaticVarSubstitution,
173        TransformPassKind::TreeShakeClass,
174        TransformPassKind::TreeShakeKeyframes,
175        TransformPassKind::TreeShakeValue,
176        TransformPassKind::TreeShakeCustomProperty,
177        TransformPassKind::DeadMediaBranchRemoval,
178        TransformPassKind::DeadSupportsBranchRemoval,
179        TransformPassKind::DesignTokenRouting,
180        TransformPassKind::PrintCss,
181    ]
182}
183
184impl TransformPassKind {
185    pub const fn ordinal(self) -> u8 {
186        match self {
187            Self::WhitespaceStrip => 1,
188            Self::CommentStrip => 2,
189            Self::NumberCompression => 3,
190            Self::UnitNormalization => 4,
191            Self::ColorCompression => 5,
192            Self::UrlQuoteStrip => 6,
193            Self::StringQuoteNormalize => 7,
194            Self::SelectorIsWhereCompression => 8,
195            Self::ShorthandCombining => 9,
196            Self::RuleDeduplication => 10,
197            Self::RuleMerging => 11,
198            Self::SelectorMerging => 12,
199            Self::EmptyRuleRemoval => 13,
200            Self::VendorPrefixing => 14,
201            Self::StalePrefixRemoval => 15,
202            Self::LightDarkLowering => 16,
203            Self::ColorMixLowering => 17,
204            Self::OklchOklabLowering => 18,
205            Self::ColorFunctionLowering => 19,
206            Self::LogicalToPhysical => 20,
207            Self::NestingUnwrap => 21,
208            Self::ScopeFlatten => 22,
209            Self::LayerFlatten => 23,
210            Self::SupportsStaticEval => 24,
211            Self::MediaStaticEval => 25,
212            Self::CalcReduction => 26,
213            Self::ImportInline => 27,
214            Self::ScssModuleEvaluate => 28,
215            Self::LessModuleEvaluate => 29,
216            Self::HashCssModuleClassNames => 30,
217            Self::ResolveCssModulesComposes => 31,
218            Self::ValueResolution => 32,
219            Self::StaticVarSubstitution => 33,
220            Self::TreeShakeClass => 34,
221            Self::TreeShakeKeyframes => 35,
222            Self::TreeShakeValue => 36,
223            Self::TreeShakeCustomProperty => 37,
224            Self::DeadMediaBranchRemoval => 38,
225            Self::DeadSupportsBranchRemoval => 39,
226            Self::DesignTokenRouting => 40,
227            Self::PrintCss => 41,
228            Self::RelativeColorLowering => 42,
229            Self::ContainerStaticEval => 43,
230            Self::NativeCssStaticEval => 44,
231        }
232    }
233
234    pub const fn label(self) -> &'static str {
235        self.id()
236    }
237
238    pub const fn title(self) -> &'static str {
239        match self {
240            Self::WhitespaceStrip => "whitespace strip",
241            Self::CommentStrip => "comment strip",
242            Self::NumberCompression => "number compression",
243            Self::UnitNormalization => "unit normalization",
244            Self::ColorCompression => "color compression",
245            Self::UrlQuoteStrip => "url quote strip",
246            Self::StringQuoteNormalize => "string and font value normalize",
247            Self::SelectorIsWhereCompression => "selector alias compression",
248            Self::ShorthandCombining => "shorthand combining",
249            Self::RuleDeduplication => "rule deduplication",
250            Self::RuleMerging => "rule merging",
251            Self::SelectorMerging => "selector merging",
252            Self::EmptyRuleRemoval => "empty rule removal",
253            Self::VendorPrefixing => "vendor prefixing",
254            Self::StalePrefixRemoval => "stale prefix removal",
255            Self::LightDarkLowering => "light-dark lowering",
256            Self::ColorMixLowering => "color-mix lowering",
257            Self::OklchOklabLowering => "oklch/oklab lowering",
258            Self::ColorFunctionLowering => "color() lowering",
259            Self::RelativeColorLowering => "relative color lowering",
260            Self::LogicalToPhysical => "logical to physical",
261            Self::NestingUnwrap => "nesting unwrap",
262            Self::ScopeFlatten => "@scope flatten",
263            Self::LayerFlatten => "@layer flatten",
264            Self::SupportsStaticEval => "@supports static eval",
265            Self::MediaStaticEval => "@media static eval",
266            Self::ContainerStaticEval => "@container static eval",
267            Self::NativeCssStaticEval => "native CSS static eval",
268            Self::CalcReduction => "calc() reduction",
269            Self::ImportInline => "@import inline",
270            Self::ScssModuleEvaluate => "SCSS module evaluate",
271            Self::LessModuleEvaluate => "Less module evaluate",
272            Self::HashCssModuleClassNames => "CSS Modules class hashing",
273            Self::ResolveCssModulesComposes => "composes resolution",
274            Self::ValueResolution => "@value resolution",
275            Self::StaticVarSubstitution => "custom property static resolve",
276            Self::TreeShakeClass => "tree shaking class",
277            Self::TreeShakeKeyframes => "tree shaking keyframes",
278            Self::TreeShakeValue => "tree shaking value",
279            Self::TreeShakeCustomProperty => "tree shaking custom-property",
280            Self::DeadMediaBranchRemoval => "dead @media branch removal",
281            Self::DeadSupportsBranchRemoval => "dead @supports branch removal",
282            Self::DesignTokenRouting => "design-token routing",
283            Self::PrintCss => "printer + sourcemap composer",
284        }
285    }
286
287    pub const fn id(self) -> &'static str {
288        match self {
289            Self::WhitespaceStrip => "whitespace-strip",
290            Self::CommentStrip => "comment-strip",
291            Self::NumberCompression => "number-compression",
292            Self::UnitNormalization => "unit-normalization",
293            Self::ColorCompression => "color-compression",
294            Self::UrlQuoteStrip => "url-quote-strip",
295            Self::StringQuoteNormalize => "string-quote-normalize",
296            Self::SelectorIsWhereCompression => "selector-is-where-compression",
297            Self::ShorthandCombining => "shorthand-combining",
298            Self::RuleDeduplication => "rule-deduplication",
299            Self::RuleMerging => "rule-merging",
300            Self::SelectorMerging => "selector-merging",
301            Self::EmptyRuleRemoval => "empty-rule-removal",
302            Self::VendorPrefixing => "vendor-prefixing",
303            Self::StalePrefixRemoval => "stale-prefix-removal",
304            Self::LightDarkLowering => "light-dark-lowering",
305            Self::ColorMixLowering => "color-mix-lowering",
306            Self::OklchOklabLowering => "oklch-oklab-lowering",
307            Self::ColorFunctionLowering => "color-function-lowering",
308            Self::RelativeColorLowering => "relative-color-lowering",
309            Self::LogicalToPhysical => "logical-to-physical",
310            Self::NestingUnwrap => "nesting-unwrap",
311            Self::ScopeFlatten => "scope-flatten",
312            Self::LayerFlatten => "layer-flatten",
313            Self::SupportsStaticEval => "supports-static-eval",
314            Self::MediaStaticEval => "media-static-eval",
315            Self::ContainerStaticEval => "container-static-eval",
316            Self::NativeCssStaticEval => "native-css-static-eval",
317            Self::CalcReduction => "calc-reduction",
318            Self::ImportInline => "import-inline",
319            Self::ScssModuleEvaluate => "scss-module-evaluate",
320            Self::LessModuleEvaluate => "less-module-evaluate",
321            Self::HashCssModuleClassNames => "css-modules-class-hashing",
322            Self::ResolveCssModulesComposes => "composes-resolution",
323            Self::ValueResolution => "value-resolution",
324            Self::StaticVarSubstitution => "custom-property-static-resolve",
325            Self::TreeShakeClass => "tree-shake-class",
326            Self::TreeShakeKeyframes => "tree-shake-keyframes",
327            Self::TreeShakeValue => "tree-shake-value",
328            Self::TreeShakeCustomProperty => "tree-shake-custom-property",
329            Self::DeadMediaBranchRemoval => "dead-media-branch-removal",
330            Self::DeadSupportsBranchRemoval => "dead-supports-branch-removal",
331            Self::DesignTokenRouting => "design-token-routing",
332            Self::PrintCss => "print-css",
333        }
334    }
335
336    pub const fn layer(self) -> TransformLayer {
337        match self {
338            Self::ImportInline
339            | Self::ScssModuleEvaluate
340            | Self::LessModuleEvaluate
341            | Self::HashCssModuleClassNames
342            | Self::ResolveCssModulesComposes
343            | Self::ValueResolution
344            | Self::StaticVarSubstitution
345            | Self::TreeShakeClass
346            | Self::TreeShakeKeyframes
347            | Self::TreeShakeValue
348            | Self::TreeShakeCustomProperty
349            | Self::DeadMediaBranchRemoval
350            | Self::DeadSupportsBranchRemoval
351            | Self::DesignTokenRouting => TransformLayer::SemanticAware,
352            Self::PrintCss => TransformLayer::Emission,
353            _ => TransformLayer::Commodity,
354        }
355    }
356
357    pub const fn reads_semantic_graph(self) -> bool {
358        matches!(
359            self,
360            Self::ImportInline
361                | Self::ScssModuleEvaluate
362                | Self::LessModuleEvaluate
363                | Self::HashCssModuleClassNames
364                | Self::ResolveCssModulesComposes
365                | Self::ValueResolution
366                | Self::StaticVarSubstitution
367                | Self::TreeShakeClass
368                | Self::TreeShakeKeyframes
369                | Self::TreeShakeValue
370                | Self::TreeShakeCustomProperty
371                | Self::DeadMediaBranchRemoval
372                | Self::DeadSupportsBranchRemoval
373                | Self::DesignTokenRouting
374        )
375    }
376
377    pub const fn reads_cascade_model(self) -> bool {
378        matches!(
379            self,
380            Self::ShorthandCombining
381                | Self::RuleDeduplication
382                | Self::RuleMerging
383                | Self::SelectorMerging
384                | Self::ScopeFlatten
385                | Self::LayerFlatten
386                | Self::StaticVarSubstitution
387                | Self::DeadMediaBranchRemoval
388                | Self::DeadSupportsBranchRemoval
389        )
390    }
391
392    pub const fn read_model(self) -> TransformPassReadModel {
393        match self {
394            Self::VendorPrefixing
395            | Self::StalePrefixRemoval
396            | Self::LightDarkLowering
397            | Self::ColorMixLowering
398            | Self::OklchOklabLowering
399            | Self::ColorFunctionLowering
400            | Self::RelativeColorLowering
401            | Self::LogicalToPhysical
402            | Self::NestingUnwrap
403            | Self::NativeCssStaticEval => TransformPassReadModel::TargetData,
404            Self::ShorthandCombining
405            | Self::RuleDeduplication
406            | Self::RuleMerging
407            | Self::SelectorMerging
408            | Self::ScopeFlatten
409            | Self::LayerFlatten
410            | Self::StaticVarSubstitution
411            | Self::DeadMediaBranchRemoval
412            | Self::DeadSupportsBranchRemoval => TransformPassReadModel::CascadeModel,
413            Self::TreeShakeClass
414            | Self::TreeShakeKeyframes
415            | Self::TreeShakeValue
416            | Self::TreeShakeCustomProperty
417            | Self::DesignTokenRouting => TransformPassReadModel::BridgeReachability,
418            Self::ImportInline
419            | Self::ScssModuleEvaluate
420            | Self::LessModuleEvaluate
421            | Self::HashCssModuleClassNames
422            | Self::ResolveCssModulesComposes
423            | Self::ValueResolution => TransformPassReadModel::SemanticGraph,
424            Self::PrintCss => TransformPassReadModel::Emission,
425            _ => TransformPassReadModel::SyntaxOnly,
426        }
427    }
428
429    pub const fn explicit_opt_in_required(self) -> bool {
430        matches!(self, Self::NativeCssStaticEval)
431    }
432
433    pub const fn dialect_restriction(self) -> Option<&'static str> {
434        match self {
435            Self::NativeCssStaticEval => Some(NATIVE_CSS_STATIC_EVAL_DIALECT_RESTRICTION_V0),
436            _ => None,
437        }
438    }
439
440    pub const fn spec_snapshot(self) -> Option<&'static str> {
441        match self {
442            Self::NativeCssStaticEval => Some(NATIVE_CSS_STATIC_EVAL_SPEC_SNAPSHOT_V0),
443            _ => None,
444        }
445    }
446
447    pub const fn opt_in_policy(self) -> Option<&'static str> {
448        match self {
449            Self::NativeCssStaticEval => Some(NATIVE_CSS_STATIC_EVAL_OPT_IN_POLICY_V0),
450            _ => None,
451        }
452    }
453}
454
455thread_local! {
456    static TRANSFORM_PASS_SORT_ORDINAL_OVERRIDES: RefCell<Option<[u8; TRANSFORM_PASS_CATALOG_LEN]>> =
457        const { RefCell::new(None) };
458    static STABLE_NODE_KEY_STAMP_COUNT: RefCell<usize> = const { RefCell::new(0) };
459}
460
461pub fn transform_pass_sort_ordinal(kind: TransformPassKind) -> u8 {
462    TRANSFORM_PASS_SORT_ORDINAL_OVERRIDES.with(|overrides| {
463        overrides
464            .borrow()
465            .as_ref()
466            .map(|values| values[(kind.ordinal() - 1) as usize])
467            .unwrap_or_else(|| kind.ordinal())
468    })
469}
470
471#[doc(hidden)]
472pub fn with_transform_pass_sort_ordinal_overrides_for_test<R>(
473    overrides: [u8; TRANSFORM_PASS_CATALOG_LEN],
474    run: impl FnOnce() -> R,
475) -> R {
476    struct ResetOrdinalOverrides(Option<[u8; TRANSFORM_PASS_CATALOG_LEN]>);
477
478    impl Drop for ResetOrdinalOverrides {
479        fn drop(&mut self) {
480            let previous = self.0.take();
481            TRANSFORM_PASS_SORT_ORDINAL_OVERRIDES.with(|overrides| {
482                overrides.replace(previous);
483            });
484        }
485    }
486
487    let previous =
488        TRANSFORM_PASS_SORT_ORDINAL_OVERRIDES.with(|values| values.replace(Some(overrides)));
489    let _reset = ResetOrdinalOverrides(previous);
490    run()
491}
492
493#[doc(hidden)]
494pub fn reset_stable_node_key_stamp_count_for_test() {
495    STABLE_NODE_KEY_STAMP_COUNT.with(|count| {
496        *count.borrow_mut() = 0;
497    });
498}
499
500#[doc(hidden)]
501pub fn stable_node_key_stamp_count_for_test() -> usize {
502    STABLE_NODE_KEY_STAMP_COUNT.with(|count| *count.borrow())
503}
504
505#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize)]
506#[serde(rename_all = "camelCase")]
507pub enum TransformPassReadModel {
508    SyntaxOnly,
509    TargetData,
510    CascadeModel,
511    SemanticGraph,
512    BridgeReachability,
513    Emission,
514}
515
516#[derive(Debug, Clone, PartialEq, Eq, Serialize)]
517#[serde(rename_all = "camelCase")]
518pub struct TransformPassContractV0 {
519    pub ordinal: u8,
520    pub label: &'static str,
521    pub id: &'static str,
522    pub title: &'static str,
523    pub kind: TransformPassKind,
524    pub family: &'static str,
525    pub execution_phase: u8,
526    pub executes_mutation: bool,
527    pub layer: TransformLayer,
528    pub read_model: TransformPassReadModel,
529    pub reads_semantic_graph: bool,
530    pub reads_cascade_model: bool,
531    pub writes_css: bool,
532    pub cascade_safety_witness: CascadeSafetyWitnessV0,
533    pub cascade_obligation: &'static str,
534    pub explicit_opt_in_required: bool,
535    pub dialect_restriction: Option<&'static str>,
536    pub spec_snapshot: Option<&'static str>,
537    pub opt_in_policy: Option<&'static str>,
538}
539
540#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize)]
541#[serde(rename_all = "camelCase")]
542pub struct CascadeSafetyWitnessV0 {
543    pub pass_id: &'static str,
544    pub obligation: &'static str,
545    pub enforced_at: &'static str,
546}
547
548impl CascadeSafetyWitnessV0 {
549    pub fn evidence_node_key(&self) -> EvidenceNodeKeyV0 {
550        EvidenceNodeKeyV0::new(CASCADE_WITNESS_EVIDENCE_QUERY_V0, self.pass_id)
551    }
552
553    pub fn evidence_node_seed(&self) -> EvidenceNodeSeedV0 {
554        let provenance = vec![
555            ["pass:", self.pass_id].concat(),
556            ["obligation:", self.obligation].concat(),
557            ["enforcedAt:", self.enforced_at].concat(),
558        ];
559        let family_stamp = prose_obligation_family_stamp(&provenance);
560        EvidenceNodeSeedV0::with_family(
561            self.evidence_node_key(),
562            provenance,
563            GuaranteeKindV0::for_label_less_family(),
564            family_stamp,
565        )
566    }
567
568    pub fn evidence_graph(&self) -> Result<EvidenceGraphV0, EvidenceGraphBuildErrorV0> {
569        build_evidence_graph_from_edges_v0(
570            [self.evidence_node_seed()],
571            [EvidenceDemandEdgeV0::new(
572                CASCADE_WITNESS_EVIDENCE_QUERY_V0,
573                self.evidence_node_key(),
574                CASCADE_WITNESS_EVIDENCE_EDGE_KIND_V0,
575            )],
576        )
577    }
578}
579
580#[derive(Debug, Clone, PartialEq, Eq, Serialize)]
581#[serde(rename_all = "camelCase")]
582pub struct TransformDagEdgeV0 {
583    pub from: &'static str,
584    pub to: &'static str,
585    pub reason: &'static str,
586}
587
588#[derive(Debug, Clone, PartialEq, Eq, Serialize)]
589#[serde(rename_all = "camelCase")]
590pub struct TransformCstBoundarySummaryV0 {
591    pub schema_version: &'static str,
592    pub product: &'static str,
593    pub representation: &'static str,
594    pub pass_contracts: Vec<TransformPassContractV0>,
595    pub pass_descriptors: Vec<TransformPassDescriptorV0>,
596    pub pass_observation_records: Vec<TransformPassObservationRecordV0>,
597    pub dag_edges: Vec<TransformDagEdgeV0>,
598    pub pass_catalog_count: usize,
599    pub pass_observation_record_count: usize,
600    pub semantic_aware_pass_count: usize,
601    pub commodity_pass_count: usize,
602    pub emission_pass_count: usize,
603    pub structural_pass_count: usize,
604    pub text_local_pass_count: usize,
605    pub module_evaluation_pass_count: usize,
606    pub full_pass_catalog_covered: bool,
607    pub all_passes_have_observation_surface: bool,
608    pub all_observation_gaps_are_reasoned: bool,
609    pub all_passes_declare_cascade_obligation: bool,
610    pub all_passes_have_compile_time_cascade_witness: bool,
611    pub stable_transform_ir_ready: bool,
612    pub provenance_derivation_forest_scaffold_ready: bool,
613    pub provenance_preservation_required: bool,
614    pub next_surfaces: Vec<&'static str>,
615}
616
617#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Serialize)]
618#[serde(rename_all = "camelCase")]
619pub enum StableTransformIrNodeKindV0 {
620    ClassSelector,
621    IdSelector,
622    PlaceholderSelector,
623    CustomPropertyDeclaration,
624    CustomPropertyReference,
625    ScssVariableDeclaration,
626    ScssVariableReference,
627    LessVariableDeclaration,
628    LessVariableReference,
629    SassSymbolDeclaration,
630    SassSymbolReference,
631    SassModuleEdge,
632    KeyframesDeclaration,
633    AnimationNameReference,
634    CssModuleValueDefinition,
635    CssModuleValueReference,
636    CssModuleValueImportSource,
637    CssModuleComposesTarget,
638    CssModuleComposesImportSource,
639    IcssExportName,
640    IcssImportLocalName,
641    IcssImportRemoteName,
642    IcssImportSource,
643    AtRule,
644}
645
646impl StableTransformIrNodeKindV0 {
647    pub const fn id(self) -> &'static str {
648        match self {
649            Self::ClassSelector => "class-selector",
650            Self::IdSelector => "id-selector",
651            Self::PlaceholderSelector => "placeholder-selector",
652            Self::CustomPropertyDeclaration => "custom-property-declaration",
653            Self::CustomPropertyReference => "custom-property-reference",
654            Self::ScssVariableDeclaration => "scss-variable-declaration",
655            Self::ScssVariableReference => "scss-variable-reference",
656            Self::LessVariableDeclaration => "less-variable-declaration",
657            Self::LessVariableReference => "less-variable-reference",
658            Self::SassSymbolDeclaration => "sass-symbol-declaration",
659            Self::SassSymbolReference => "sass-symbol-reference",
660            Self::SassModuleEdge => "sass-module-edge",
661            Self::KeyframesDeclaration => "keyframes-declaration",
662            Self::AnimationNameReference => "animation-name-reference",
663            Self::CssModuleValueDefinition => "css-module-value-definition",
664            Self::CssModuleValueReference => "css-module-value-reference",
665            Self::CssModuleValueImportSource => "css-module-value-import-source",
666            Self::CssModuleComposesTarget => "css-module-composes-target",
667            Self::CssModuleComposesImportSource => "css-module-composes-import-source",
668            Self::IcssExportName => "icss-export-name",
669            Self::IcssImportLocalName => "icss-import-local-name",
670            Self::IcssImportRemoteName => "icss-import-remote-name",
671            Self::IcssImportSource => "icss-import-source",
672            Self::AtRule => "at-rule",
673        }
674    }
675}
676
677#[derive(Debug, Clone, PartialEq, Eq, PartialOrd, Ord, Hash, Serialize)]
678#[serde(transparent)]
679pub struct StableNodeKeyV0(pub String);
680
681impl StableNodeKeyV0 {
682    pub fn as_str(&self) -> &str {
683        self.0.as_str()
684    }
685}
686
687#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Hash, Serialize)]
688#[serde(transparent)]
689pub struct StableNodeKeyU64V0(pub u64);
690
691impl StableNodeKeyU64V0 {
692    pub const fn as_u64(self) -> u64 {
693        self.0
694    }
695}
696
697#[derive(Debug, Clone, PartialEq, Eq)]
698struct StableNodeKeySeedV0 {
699    semantic_key: String,
700    ordinal: usize,
701}
702
703impl StableNodeKeySeedV0 {
704    fn new(semantic_key: String, ordinal: usize) -> Self {
705        Self {
706            semantic_key,
707            ordinal,
708        }
709    }
710
711    fn materialize(&self) -> StableNodeKeyV0 {
712        STABLE_NODE_KEY_STAMP_COUNT.with(|count| {
713            *count.borrow_mut() += 1;
714        });
715        StableNodeKeyV0(format!("{}#{}", self.semantic_key, self.ordinal))
716    }
717
718    fn materialize_u64(&self) -> StableNodeKeyU64V0 {
719        let mut hash = StableNodeKeyFnv64::new();
720        hash.piece("omena-transform-cst.stable-node-key");
721        hash.piece(&self.semantic_key);
722        hash.piece("#");
723        let ordinal = self.ordinal.to_string();
724        hash.piece(&ordinal);
725        StableNodeKeyU64V0(hash.finish())
726    }
727}
728
729struct StableNodeKeyFnv64(u64);
730
731impl StableNodeKeyFnv64 {
732    const OFFSET: u64 = 0xcbf29ce484222325;
733    const PRIME: u64 = 0x00000100000001b3;
734
735    const fn new() -> Self {
736        Self(Self::OFFSET)
737    }
738
739    fn piece(&mut self, value: &str) {
740        for byte in value.as_bytes() {
741            self.0 = (self.0 ^ u64::from(*byte)).wrapping_mul(Self::PRIME);
742        }
743        self.0 = (self.0 ^ 0xff).wrapping_mul(Self::PRIME);
744    }
745
746    const fn finish(self) -> u64 {
747        self.0
748    }
749}
750
751#[derive(Debug, Clone, PartialEq, Eq)]
752pub struct StableTransformIrNodeV0 {
753    pub node_id: String,
754    node_key: OnceLock<StableNodeKeyV0>,
755    node_key_u64: OnceLock<StableNodeKeyU64V0>,
756    node_key_seed: Option<StableNodeKeySeedV0>,
757    pub kind: StableTransformIrNodeKindV0,
758    pub kind_id: &'static str,
759    pub label: String,
760    pub semantic_key: String,
761    pub source_span_start: usize,
762    pub source_span_end: usize,
763    pub provenance_anchor_index: usize,
764}
765
766impl StableTransformIrNodeV0 {
767    pub fn positional_node_id(&self) -> &str {
768        self.node_id.as_str()
769    }
770
771    pub fn additive_node_key(&self) -> Option<&StableNodeKeyV0> {
772        if let Some(key) = self.node_key.get() {
773            return Some(key);
774        }
775        let seed = self.node_key_seed.as_ref()?;
776        Some(self.node_key.get_or_init(|| seed.materialize()))
777    }
778
779    pub fn additive_node_key_u64(&self) -> Option<StableNodeKeyU64V0> {
780        if let Some(key) = self.node_key_u64.get() {
781            return Some(*key);
782        }
783        let seed = self.node_key_seed.as_ref()?;
784        Some(*self.node_key_u64.get_or_init(|| seed.materialize_u64()))
785    }
786
787    fn set_additive_node_key_seed(&mut self, ordinal: usize) {
788        self.node_key = OnceLock::new();
789        self.node_key_u64 = OnceLock::new();
790        self.node_key_seed = Some(StableNodeKeySeedV0::new(self.semantic_key.clone(), ordinal));
791    }
792
793    #[doc(hidden)]
794    pub fn clear_additive_node_key_for_test(&mut self) {
795        self.node_key = OnceLock::new();
796        self.node_key_u64 = OnceLock::new();
797        self.node_key_seed = None;
798    }
799}
800
801impl Serialize for StableTransformIrNodeV0 {
802    fn serialize<S>(&self, serializer: S) -> Result<S::Ok, S::Error>
803    where
804        S: serde::Serializer,
805    {
806        let node_key = self.additive_node_key();
807        let node_key_u64 = self.additive_node_key_u64();
808        let field_count = 8 + usize::from(node_key.is_some()) + usize::from(node_key_u64.is_some());
809        let mut state = serializer.serialize_struct("StableTransformIrNodeV0", field_count)?;
810        state.serialize_field("nodeId", &self.node_id)?;
811        if let Some(node_key) = node_key {
812            state.serialize_field("nodeKey", node_key)?;
813        }
814        if let Some(node_key_u64) = node_key_u64 {
815            state.serialize_field("nodeKeyU64", &node_key_u64)?;
816        }
817        state.serialize_field("kind", &self.kind)?;
818        state.serialize_field("kindId", &self.kind_id)?;
819        state.serialize_field("label", &self.label)?;
820        state.serialize_field("semanticKey", &self.semantic_key)?;
821        state.serialize_field("sourceSpanStart", &self.source_span_start)?;
822        state.serialize_field("sourceSpanEnd", &self.source_span_end)?;
823        state.serialize_field("provenanceAnchorIndex", &self.provenance_anchor_index)?;
824        state.end()
825    }
826}
827
828#[derive(Debug, Clone, PartialEq, Eq, Serialize)]
829#[serde(rename_all = "camelCase")]
830pub struct TransformCstProvenanceAnchorV0 {
831    pub anchor_index: usize,
832    pub node_id: String,
833    pub semantic_key: String,
834    pub source_span_start: usize,
835    pub source_span_end: usize,
836}
837
838#[derive(Debug, Clone, PartialEq, Eq, Serialize)]
839#[serde(rename_all = "camelCase")]
840pub struct StableTransformIrV0 {
841    pub schema_version: &'static str,
842    pub product: &'static str,
843    pub dialect: &'static str,
844    pub source_byte_len: usize,
845    pub semantic_signature: String,
846    pub node_count: usize,
847    pub parser_error_count: usize,
848    pub contains_bogus_or_trivia: bool,
849    pub stable_post_semantic_ir: bool,
850    pub nodes: Vec<StableTransformIrNodeV0>,
851    pub provenance_anchors: Vec<TransformCstProvenanceAnchorV0>,
852}
853
854pub const STABLE_TRANSFORM_IR_SCHEMA_VERSION_V0: &str = "0";
855
856pub const STABLE_TRANSFORM_IR_NODE_IDENTITY_POLICY_V0: &str =
857    "schema-v0-node-key-preferred-node-id-fallback";
858
859impl StableTransformIrV0 {
860    pub fn node_identity_policy(&self) -> &'static str {
861        if self.schema_version == STABLE_TRANSFORM_IR_SCHEMA_VERSION_V0 {
862            STABLE_TRANSFORM_IR_NODE_IDENTITY_POLICY_V0
863        } else {
864            "legacy-node-id-only"
865        }
866    }
867
868    pub fn identity_key_for_node<'a>(&self, node: &'a StableTransformIrNodeV0) -> Cow<'a, str> {
869        if self.schema_version == STABLE_TRANSFORM_IR_SCHEMA_VERSION_V0
870            && let Some(node_key) = node.additive_node_key()
871        {
872            return Cow::Borrowed(node_key.as_str());
873        }
874        Cow::Borrowed(node.positional_node_id())
875    }
876
877    pub fn identity_key_at(&self, node_index: usize) -> Option<Cow<'_, str>> {
878        self.nodes
879            .get(node_index)
880            .map(|node| self.identity_key_for_node(node))
881    }
882}
883
884#[derive(Debug, Clone, PartialEq, Eq, Serialize)]
885#[serde(rename_all = "camelCase")]
886pub struct TransformCstArtifactV0 {
887    pub schema_version: &'static str,
888    pub product: &'static str,
889    pub source_byte_len: usize,
890    pub semantic_signature: String,
891    pub stable_ir: StableTransformIrV0,
892    pub stable_ir_node_count: usize,
893    pub parser_error_count: usize,
894    pub contains_bogus_or_trivia: bool,
895    pub pass_ids: Vec<&'static str>,
896    pub provenance_preserved: bool,
897}
898
899#[derive(Debug, Clone, PartialEq, Eq, Serialize)]
900#[serde(rename_all = "camelCase")]
901pub struct TransformPassSpecV0 {
902    pub schema_version: &'static str,
903    pub product: &'static str,
904    pub pass_id: &'static str,
905    pub pass_kind: TransformPassKind,
906    pub cascade_obligation: &'static str,
907    pub cascade_safety_witness: CascadeSafetyWitnessV0,
908}
909
910impl TransformPassSpecV0 {
911    pub fn from_pass(pass_kind: TransformPassKind) -> Self {
912        let cascade_safety_witness = cascade_safety_witness(pass_kind);
913        Self {
914            schema_version: "0",
915            product: "omena-transform-cst.pass-spec",
916            pass_id: pass_kind.id(),
917            pass_kind,
918            cascade_obligation: cascade_safety_witness.obligation,
919            cascade_safety_witness,
920        }
921    }
922
923    pub fn declares_cascade_obligation(&self) -> bool {
924        !self.cascade_obligation.is_empty()
925            && self.cascade_safety_witness.pass_id == self.pass_id
926            && self.cascade_safety_witness.obligation == self.cascade_obligation
927    }
928}
929
930#[derive(Debug, Clone, PartialEq, Eq, Serialize)]
931#[serde(rename_all = "camelCase")]
932pub struct RewriteCandidateV0 {
933    schema_version: &'static str,
934    product: &'static str,
935    pass_spec: TransformPassSpecV0,
936    semantic_signature: String,
937    input_source_byte_len: usize,
938    output_source_byte_len: usize,
939    input_stable_ir: StableTransformIrV0,
940    output_stable_ir: StableTransformIrV0,
941}
942
943impl RewriteCandidateV0 {
944    pub fn from_sources(
945        pass_kind: TransformPassKind,
946        input_source: &str,
947        output_source: &str,
948        dialect: StyleDialect,
949        semantic_signature: impl Into<String>,
950    ) -> Self {
951        let semantic_signature = semantic_signature.into();
952        Self {
953            schema_version: "0",
954            product: "omena-transform-cst.rewrite-candidate",
955            pass_spec: TransformPassSpecV0::from_pass(pass_kind),
956            semantic_signature: semantic_signature.clone(),
957            input_source_byte_len: input_source.len(),
958            output_source_byte_len: output_source.len(),
959            input_stable_ir: build_stable_transform_ir_from_source(
960                input_source,
961                dialect,
962                semantic_signature.clone(),
963            ),
964            output_stable_ir: build_stable_transform_ir_from_source(
965                output_source,
966                dialect,
967                semantic_signature,
968            ),
969        }
970    }
971
972    pub fn pass_spec(&self) -> &TransformPassSpecV0 {
973        &self.pass_spec
974    }
975
976    pub fn output_stable_ir(&self) -> &StableTransformIrV0 {
977        &self.output_stable_ir
978    }
979}
980
981#[derive(Debug, Clone, PartialEq, Eq, Serialize)]
982#[serde(rename_all = "camelCase")]
983pub struct VerificationReportV0 {
984    schema_version: &'static str,
985    product: &'static str,
986    pass_id: &'static str,
987    closed_world_bundle_hash: Option<String>,
988    cascade_obligation_declared: bool,
989    provenance_recomputed: bool,
990    provenance_preserved: bool,
991    contains_bogus_or_trivia: bool,
992    stable_post_semantic_ir: bool,
993    cascade_proof: CascadeSMTProofV0,
994}
995
996impl VerificationReportV0 {
997    pub fn provenance_preserved(&self) -> bool {
998        self.provenance_preserved
999    }
1000
1001    pub fn contains_bogus_or_trivia(&self) -> bool {
1002        self.contains_bogus_or_trivia
1003    }
1004
1005    pub fn cascade_proof(&self) -> &CascadeSMTProofV0 {
1006        &self.cascade_proof
1007    }
1008
1009    pub fn closed_world_bundle_hash(&self) -> Option<&str> {
1010        self.closed_world_bundle_hash.as_deref()
1011    }
1012
1013    pub fn cascade_safe(&self) -> bool {
1014        self.cascade_proof.verdict == SmtVerdictV0::Accepted
1015    }
1016}
1017
1018#[derive(Debug, Clone, PartialEq, Eq, Serialize)]
1019#[serde(rename_all = "camelCase")]
1020pub struct VerifiedRewriteV0 {
1021    schema_version: &'static str,
1022    product: &'static str,
1023    candidate: RewriteCandidateV0,
1024    verification_report: VerificationReportV0,
1025}
1026
1027impl VerifiedRewriteV0 {
1028    pub fn candidate(&self) -> &RewriteCandidateV0 {
1029        &self.candidate
1030    }
1031
1032    pub fn verification_report(&self) -> &VerificationReportV0 {
1033        &self.verification_report
1034    }
1035}
1036
1037#[derive(Debug, Clone, PartialEq, Eq, Serialize)]
1038#[serde(rename_all = "camelCase")]
1039pub enum TransformVerificationErrorV0 {
1040    CascadeProofRejected {
1041        pass_id: &'static str,
1042        verdict: SmtVerdictV0,
1043    },
1044    CascadeObligationMissing {
1045        pass_id: &'static str,
1046    },
1047    ProvenanceNotPreserved {
1048        pass_id: &'static str,
1049    },
1050    ClosedWorldBundleRequired {
1051        pass_id: &'static str,
1052    },
1053}
1054
1055pub fn summarize_omena_transform_cst_boundary() -> TransformCstBoundarySummaryV0 {
1056    let pass_contracts = default_transform_pass_contracts();
1057    let pass_descriptors = default_transform_pass_descriptors();
1058    let pass_observation_records = default_transform_pass_observation_records();
1059    let semantic_aware_pass_count = pass_contracts
1060        .iter()
1061        .filter(|contract| contract.layer == TransformLayer::SemanticAware)
1062        .count();
1063    let commodity_pass_count = pass_contracts
1064        .iter()
1065        .filter(|contract| contract.layer == TransformLayer::Commodity)
1066        .count();
1067    let emission_pass_count = pass_contracts
1068        .iter()
1069        .filter(|contract| contract.layer == TransformLayer::Emission)
1070        .count();
1071    let structural_pass_count = pass_descriptors
1072        .iter()
1073        .filter(|descriptor| descriptor.pass_class == TransformPassClassV0::Structural)
1074        .count();
1075    let text_local_pass_count = pass_descriptors
1076        .iter()
1077        .filter(|descriptor| descriptor.pass_class == TransformPassClassV0::TextLocal)
1078        .count();
1079    let module_evaluation_pass_count = pass_descriptors
1080        .iter()
1081        .filter(|descriptor| descriptor.pass_class == TransformPassClassV0::ModuleEvaluation)
1082        .count();
1083    let all_passes_declare_cascade_obligation = pass_contracts
1084        .iter()
1085        .all(|contract| !contract.cascade_obligation.is_empty());
1086    let all_passes_have_compile_time_cascade_witness = pass_contracts.iter().all(|contract| {
1087        contract.cascade_safety_witness.pass_id == contract.id
1088            && contract.cascade_safety_witness.obligation == contract.cascade_obligation
1089            && contract.cascade_safety_witness.enforced_at == "compile-time-exhaustive-pass-catalog"
1090    });
1091    let all_passes_have_observation_surface = pass_observation_records.len()
1092        == TRANSFORM_PASS_CATALOG_LEN
1093        && pass_observation_records.iter().all(|record| {
1094            record.id == record.kind.id()
1095                && matches!(
1096                    record.surface,
1097                    PassObservationSurfaceV0::Declared(_)
1098                        | PassObservationSurfaceV0::UnknownGap { .. }
1099                )
1100        });
1101    let all_observation_gaps_are_reasoned = pass_observation_records.iter().all(|record| {
1102        record
1103            .surface
1104            .gap_reason()
1105            .map(|reason| !reason.trim().is_empty())
1106            .unwrap_or(true)
1107    });
1108    let pass_catalog_count = pass_contracts.len();
1109    let pass_observation_record_count = pass_observation_records.len();
1110
1111    TransformCstBoundarySummaryV0 {
1112        schema_version: "0",
1113        product: "omena-transform-cst.boundary",
1114        representation: "post-semantic-provenance-preserving-transform-cst",
1115        pass_contracts,
1116        pass_descriptors,
1117        pass_observation_records,
1118        dag_edges: default_transform_dag_edges(),
1119        pass_catalog_count,
1120        pass_observation_record_count,
1121        semantic_aware_pass_count,
1122        commodity_pass_count,
1123        emission_pass_count,
1124        structural_pass_count,
1125        text_local_pass_count,
1126        module_evaluation_pass_count,
1127        full_pass_catalog_covered: pass_catalog_count == TRANSFORM_PASS_CATALOG_LEN,
1128        all_passes_have_observation_surface,
1129        all_observation_gaps_are_reasoned,
1130        all_passes_declare_cascade_obligation,
1131        all_passes_have_compile_time_cascade_witness,
1132        stable_transform_ir_ready: true,
1133        provenance_derivation_forest_scaffold_ready: true,
1134        provenance_preservation_required: true,
1135        next_surfaces: Vec::new(),
1136    }
1137}
1138
1139pub fn build_transform_cst_artifact(
1140    source: &str,
1141    semantic_signature: impl Into<String>,
1142    passes: &[TransformPassKind],
1143) -> TransformCstArtifactV0 {
1144    build_transform_cst_artifact_with_dialect(source, StyleDialect::Css, semantic_signature, passes)
1145}
1146
1147pub fn build_transform_cst_artifact_with_dialect(
1148    source: &str,
1149    dialect: StyleDialect,
1150    semantic_signature: impl Into<String>,
1151    passes: &[TransformPassKind],
1152) -> TransformCstArtifactV0 {
1153    let semantic_signature = semantic_signature.into();
1154    let stable_ir =
1155        build_stable_transform_ir_from_source(source, dialect, semantic_signature.clone());
1156    let stable_ir_node_count = stable_ir.node_count;
1157    let parser_error_count = stable_ir.parser_error_count;
1158    let contains_bogus_or_trivia = stable_ir.contains_bogus_or_trivia;
1159    let verified_rewrites =
1160        verify_rewrite_plan_with_backend(source, dialect, semantic_signature.clone(), passes);
1161    let provenance_preserved = verified_rewrites.as_ref().is_ok_and(|rewrites| {
1162        rewrites
1163            .iter()
1164            .all(|rewrite| rewrite.verification_report.provenance_preserved)
1165    });
1166
1167    TransformCstArtifactV0 {
1168        schema_version: "0",
1169        product: "omena-transform-cst.artifact",
1170        source_byte_len: source.len(),
1171        semantic_signature,
1172        stable_ir,
1173        stable_ir_node_count,
1174        parser_error_count,
1175        contains_bogus_or_trivia,
1176        pass_ids: passes.iter().map(|pass| pass.id()).collect(),
1177        provenance_preserved,
1178    }
1179}
1180
1181pub fn build_verified_transform_cst_artifact_with_dialect(
1182    source: &str,
1183    dialect: StyleDialect,
1184    semantic_signature: impl Into<String>,
1185    passes: &[TransformPassKind],
1186) -> Result<TransformCstArtifactV0, TransformVerificationErrorV0> {
1187    let semantic_signature = semantic_signature.into();
1188    let verified_rewrites =
1189        verify_rewrite_plan_with_backend(source, dialect, semantic_signature.clone(), passes)?;
1190    let stable_ir =
1191        build_stable_transform_ir_from_source(source, dialect, semantic_signature.clone());
1192    Ok(transform_cst_artifact_from_verified_plan(
1193        source.len(),
1194        semantic_signature,
1195        stable_ir,
1196        passes,
1197        &verified_rewrites,
1198    ))
1199}
1200
1201pub fn verify_rewrite_candidate_with_backend<B: SmtBackendV0>(
1202    candidate: RewriteCandidateV0,
1203    backend: &B,
1204) -> Result<VerifiedRewriteV0, TransformVerificationErrorV0> {
1205    verify_rewrite_candidate_inner(candidate, backend, None)
1206}
1207
1208pub fn verify_rewrite_candidate_with_backend_and_closed_world_bundle<B: SmtBackendV0>(
1209    candidate: RewriteCandidateV0,
1210    backend: &B,
1211    closed_world_bundle: &ClosedWorldBundleV0,
1212) -> Result<VerifiedRewriteV0, TransformVerificationErrorV0> {
1213    verify_rewrite_candidate_inner(candidate, backend, Some(closed_world_bundle))
1214}
1215
1216fn verify_rewrite_candidate_inner<B: SmtBackendV0>(
1217    candidate: RewriteCandidateV0,
1218    backend: &B,
1219    closed_world_bundle: Option<&ClosedWorldBundleV0>,
1220) -> Result<VerifiedRewriteV0, TransformVerificationErrorV0> {
1221    let pass_id = candidate.pass_spec.pass_id;
1222    if closed_world_bundle.is_none()
1223        && transform_pass_requires_closed_world_bundle(candidate.pass_spec.pass_kind)
1224    {
1225        return Err(TransformVerificationErrorV0::ClosedWorldBundleRequired { pass_id });
1226    }
1227    let obligation_family = obligation_family_for_transform_pass(candidate.pass_spec.pass_kind);
1228    let cascade_obligation_declared = obligation_family.declares_cascade_obligation();
1229    let provenance_recomputed = candidate_recomputes_provenance(&candidate);
1230    let contains_bogus_or_trivia = candidate.input_stable_ir.contains_bogus_or_trivia
1231        || candidate.output_stable_ir.contains_bogus_or_trivia;
1232    let stable_post_semantic_ir = candidate.input_stable_ir.stable_post_semantic_ir
1233        && candidate.output_stable_ir.stable_post_semantic_ir;
1234    let provenance_preserved =
1235        provenance_recomputed && stable_post_semantic_ir && !contains_bogus_or_trivia;
1236    let proof_input = TransformRewriteProofInputV0::new(
1237        pass_id,
1238        obligation_family,
1239        provenance_recomputed,
1240        provenance_preserved,
1241        contains_bogus_or_trivia,
1242        stable_post_semantic_ir,
1243    );
1244    let cascade_proof = smt_verify_transform_rewrite_candidate_v0(&proof_input, backend);
1245    let verdict = cascade_proof.verdict;
1246    let verification_report = VerificationReportV0 {
1247        schema_version: "0",
1248        product: "omena-transform-cst.verification-report",
1249        pass_id,
1250        closed_world_bundle_hash: closed_world_bundle
1251            .map(|bundle| bundle.closure_hash().to_string()),
1252        cascade_obligation_declared,
1253        provenance_recomputed,
1254        provenance_preserved,
1255        contains_bogus_or_trivia,
1256        stable_post_semantic_ir,
1257        cascade_proof,
1258    };
1259
1260    if verdict != SmtVerdictV0::Accepted {
1261        return Err(TransformVerificationErrorV0::CascadeProofRejected { pass_id, verdict });
1262    }
1263    if !cascade_obligation_declared {
1264        return Err(TransformVerificationErrorV0::CascadeObligationMissing { pass_id });
1265    }
1266    if !provenance_preserved {
1267        return Err(TransformVerificationErrorV0::ProvenanceNotPreserved { pass_id });
1268    }
1269
1270    Ok(VerifiedRewriteV0 {
1271        schema_version: "0",
1272        product: "omena-transform-cst.verified-rewrite",
1273        candidate,
1274        verification_report,
1275    })
1276}
1277
1278pub fn verify_rewrite_candidate(
1279    candidate: RewriteCandidateV0,
1280) -> Result<VerifiedRewriteV0, TransformVerificationErrorV0> {
1281    verify_rewrite_candidate_with_backend(candidate, &StubSmtBackendV0::default())
1282}
1283
1284pub fn verify_rewrite_candidate_with_closed_world_bundle(
1285    candidate: RewriteCandidateV0,
1286    closed_world_bundle: &ClosedWorldBundleV0,
1287) -> Result<VerifiedRewriteV0, TransformVerificationErrorV0> {
1288    verify_rewrite_candidate_with_backend_and_closed_world_bundle(
1289        candidate,
1290        &StubSmtBackendV0::default(),
1291        closed_world_bundle,
1292    )
1293}
1294
1295pub fn apply_verified_rewrite(verified_rewrite: &VerifiedRewriteV0) -> TransformCstArtifactV0 {
1296    let candidate = verified_rewrite.candidate();
1297    transform_cst_artifact_from_verified_plan(
1298        candidate.output_source_byte_len,
1299        candidate.semantic_signature.clone(),
1300        candidate.output_stable_ir.clone(),
1301        &[candidate.pass_spec.pass_kind],
1302        std::slice::from_ref(verified_rewrite),
1303    )
1304}
1305
1306pub fn build_stable_transform_ir_from_source(
1307    source: &str,
1308    dialect: StyleDialect,
1309    semantic_signature: impl Into<String>,
1310) -> StableTransformIrV0 {
1311    let facts = collect_style_facts(source, dialect);
1312    let mut nodes = Vec::new();
1313
1314    for selector in facts.selectors {
1315        push_ir_node(
1316            &mut nodes,
1317            stable_ir_selector_kind(selector.kind),
1318            selector.name,
1319            selector.range.start().into(),
1320            selector.range.end().into(),
1321        );
1322    }
1323
1324    for variable in facts.variables {
1325        push_ir_node(
1326            &mut nodes,
1327            stable_ir_variable_kind(variable.kind),
1328            variable.name,
1329            variable.range.start().into(),
1330            variable.range.end().into(),
1331        );
1332    }
1333
1334    for symbol in facts.sass_symbols {
1335        push_ir_node(
1336            &mut nodes,
1337            stable_ir_sass_symbol_kind(symbol.kind),
1338            format!("{}:{}", symbol.symbol_kind, symbol.name),
1339            symbol.range.start().into(),
1340            symbol.range.end().into(),
1341        );
1342    }
1343
1344    for edge in facts.sass_module_edges {
1345        push_ir_node(
1346            &mut nodes,
1347            StableTransformIrNodeKindV0::SassModuleEdge,
1348            edge.source,
1349            edge.range.start().into(),
1350            edge.range.end().into(),
1351        );
1352    }
1353
1354    for animation in facts.animations {
1355        push_ir_node(
1356            &mut nodes,
1357            stable_ir_animation_kind(animation.kind),
1358            animation.name,
1359            animation.range.start().into(),
1360            animation.range.end().into(),
1361        );
1362    }
1363
1364    for value in facts.css_module_values {
1365        push_ir_node(
1366            &mut nodes,
1367            stable_ir_css_module_value_kind(value.kind),
1368            value.name,
1369            value.range.start().into(),
1370            value.range.end().into(),
1371        );
1372    }
1373
1374    for composes in facts.css_module_composes {
1375        push_ir_node(
1376            &mut nodes,
1377            stable_ir_css_module_composes_kind(composes.kind),
1378            composes.name,
1379            composes.range.start().into(),
1380            composes.range.end().into(),
1381        );
1382    }
1383
1384    for icss in facts.icss {
1385        push_ir_node(
1386            &mut nodes,
1387            stable_ir_icss_kind(icss.kind),
1388            icss.name,
1389            icss.range.start().into(),
1390            icss.range.end().into(),
1391        );
1392    }
1393
1394    for at_rule in facts.at_rules {
1395        push_ir_node(
1396            &mut nodes,
1397            StableTransformIrNodeKindV0::AtRule,
1398            at_rule.name,
1399            at_rule.range.start().into(),
1400            at_rule.range.end().into(),
1401        );
1402    }
1403
1404    nodes.sort_by(|left, right| {
1405        left.source_span_start
1406            .cmp(&right.source_span_start)
1407            .then_with(|| left.source_span_end.cmp(&right.source_span_end))
1408            .then_with(|| left.kind.cmp(&right.kind))
1409            .then_with(|| left.label.cmp(&right.label))
1410    });
1411
1412    let mut provenance_anchors = Vec::with_capacity(nodes.len());
1413    let mut semantic_key_ordinals = BTreeMap::new();
1414    for (index, node) in nodes.iter_mut().enumerate() {
1415        let ordinal = semantic_key_ordinals
1416            .entry(node.semantic_key.clone())
1417            .and_modify(|count| *count += 1)
1418            .or_insert(0);
1419        node.node_id = format!("ir:{index}");
1420        node.set_additive_node_key_seed(*ordinal);
1421        node.provenance_anchor_index = index;
1422        provenance_anchors.push(TransformCstProvenanceAnchorV0 {
1423            anchor_index: index,
1424            node_id: node.node_id.clone(),
1425            semantic_key: node.semantic_key.clone(),
1426            source_span_start: node.source_span_start,
1427            source_span_end: node.source_span_end,
1428        });
1429    }
1430
1431    let node_count = nodes.len();
1432    let parser_error_count = facts.error_count;
1433    let contains_bogus_or_trivia = parser_error_count > 0;
1434
1435    StableTransformIrV0 {
1436        schema_version: "0",
1437        product: "omena-transform-cst.stable-ir",
1438        dialect: transform_cst_style_dialect_label(dialect),
1439        source_byte_len: source.len(),
1440        semantic_signature: semantic_signature.into(),
1441        node_count,
1442        parser_error_count,
1443        contains_bogus_or_trivia,
1444        stable_post_semantic_ir: parser_error_count == 0,
1445        nodes,
1446        provenance_anchors,
1447    }
1448}
1449
1450fn verify_rewrite_plan_with_backend(
1451    source: &str,
1452    dialect: StyleDialect,
1453    semantic_signature: String,
1454    passes: &[TransformPassKind],
1455) -> Result<Vec<VerifiedRewriteV0>, TransformVerificationErrorV0> {
1456    let backend = StubSmtBackendV0::default();
1457    passes
1458        .iter()
1459        .map(|pass| {
1460            verify_rewrite_candidate_with_backend(
1461                RewriteCandidateV0::from_sources(
1462                    *pass,
1463                    source,
1464                    source,
1465                    dialect,
1466                    semantic_signature.clone(),
1467                ),
1468                &backend,
1469            )
1470        })
1471        .collect()
1472}
1473
1474fn transform_cst_artifact_from_verified_plan(
1475    source_byte_len: usize,
1476    semantic_signature: String,
1477    stable_ir: StableTransformIrV0,
1478    passes: &[TransformPassKind],
1479    verified_rewrites: &[VerifiedRewriteV0],
1480) -> TransformCstArtifactV0 {
1481    let stable_ir_node_count = stable_ir.node_count;
1482    let parser_error_count = stable_ir.parser_error_count;
1483    let contains_bogus_or_trivia = verified_rewrites
1484        .iter()
1485        .any(|rewrite| rewrite.verification_report.contains_bogus_or_trivia());
1486    let provenance_preserved = verified_rewrites
1487        .iter()
1488        .all(|rewrite| rewrite.verification_report.provenance_preserved());
1489
1490    TransformCstArtifactV0 {
1491        schema_version: "0",
1492        product: "omena-transform-cst.artifact",
1493        source_byte_len,
1494        semantic_signature,
1495        stable_ir,
1496        stable_ir_node_count,
1497        parser_error_count,
1498        contains_bogus_or_trivia,
1499        pass_ids: passes.iter().map(|pass| pass.id()).collect(),
1500        provenance_preserved,
1501    }
1502}
1503
1504fn candidate_recomputes_provenance(candidate: &RewriteCandidateV0) -> bool {
1505    stable_ir_has_consistent_provenance(&candidate.input_stable_ir)
1506        && stable_ir_has_consistent_provenance(&candidate.output_stable_ir)
1507}
1508
1509fn stable_ir_has_consistent_provenance(ir: &StableTransformIrV0) -> bool {
1510    ir.node_count == ir.nodes.len()
1511        && ir.node_count == ir.provenance_anchors.len()
1512        && ir.nodes.iter().enumerate().all(|(index, node)| {
1513            let Some(anchor) = ir.provenance_anchors.get(index) else {
1514                return false;
1515            };
1516            node.provenance_anchor_index == index
1517                && anchor.anchor_index == index
1518                && anchor.node_id == node.node_id
1519                && anchor.semantic_key == node.semantic_key
1520                && anchor.source_span_start == node.source_span_start
1521                && anchor.source_span_end == node.source_span_end
1522        })
1523}
1524
1525pub fn default_transform_pass_contracts() -> Vec<TransformPassContractV0> {
1526    all_transform_pass_kinds()
1527        .into_iter()
1528        .map(transform_pass_contract)
1529        .collect()
1530}
1531
1532fn transform_pass_contract(kind: TransformPassKind) -> TransformPassContractV0 {
1533    let cascade_safety_witness = cascade_safety_witness(kind);
1534
1535    TransformPassContractV0 {
1536        ordinal: kind.ordinal(),
1537        label: kind.label(),
1538        id: kind.id(),
1539        title: kind.title(),
1540        kind,
1541        family: transform_pass_family(kind),
1542        execution_phase: transform_pass_execution_phase(kind),
1543        executes_mutation: transform_pass_executes_mutation(kind),
1544        layer: kind.layer(),
1545        read_model: kind.read_model(),
1546        reads_semantic_graph: kind.reads_semantic_graph(),
1547        reads_cascade_model: kind.reads_cascade_model(),
1548        writes_css: true,
1549        cascade_safety_witness,
1550        cascade_obligation: cascade_safety_witness.obligation,
1551        explicit_opt_in_required: kind.explicit_opt_in_required(),
1552        dialect_restriction: kind.dialect_restriction(),
1553        spec_snapshot: kind.spec_snapshot(),
1554        opt_in_policy: kind.opt_in_policy(),
1555    }
1556}
1557
1558const fn transform_pass_family(kind: TransformPassKind) -> &'static str {
1559    match kind.ordinal() {
1560        1..=7 => "commodity-token",
1561        8 | 26 => "egg-backed",
1562        9..=13 => "cascade-proven-structural",
1563        14..=25 | 42..=44 => "target-lowering",
1564        27..=29 => "module-bundle",
1565        30..=33 => "css-modules-resolution",
1566        34..=40 => "semantic-reachability",
1567        41 => "emission",
1568        _ => "unknown",
1569    }
1570}
1571
1572const fn transform_pass_execution_phase(kind: TransformPassKind) -> u8 {
1573    match kind.ordinal() {
1574        27..=29 => 10,
1575        30..=40 => 20,
1576        14..=25 | 42..=44 => 30,
1577        8..=13 | 26 => 40,
1578        1..=7 => 50,
1579        41 => 60,
1580        _ => 70,
1581    }
1582}
1583
1584const fn transform_pass_executes_mutation(_kind: TransformPassKind) -> bool {
1585    true
1586}
1587
1588pub const fn cascade_safety_witness(kind: TransformPassKind) -> CascadeSafetyWitnessV0 {
1589    CascadeSafetyWitnessV0 {
1590        pass_id: kind.id(),
1591        obligation: cascade_safe_obligation(kind),
1592        enforced_at: "compile-time-exhaustive-pass-catalog",
1593    }
1594}
1595
1596pub const fn obligation_family_for_transform_pass(kind: TransformPassKind) -> ObligationFamilyIdV0 {
1597    match kind {
1598        TransformPassKind::WhitespaceStrip => ObligationFamilyIdV0::WhitespaceBoundary,
1599        TransformPassKind::CommentStrip => ObligationFamilyIdV0::CommentSourceMapProvenance,
1600        TransformPassKind::NumberCompression => ObligationFamilyIdV0::NumericLiteralEquivalence,
1601        TransformPassKind::UnitNormalization => ObligationFamilyIdV0::DimensionComputedValue,
1602        TransformPassKind::ColorCompression => ObligationFamilyIdV0::ColorLiteralEquivalence,
1603        TransformPassKind::UrlQuoteStrip => ObligationFamilyIdV0::UrlTokenGrammar,
1604        TransformPassKind::StringQuoteNormalize => ObligationFamilyIdV0::StringTextAndFontValue,
1605        TransformPassKind::SelectorIsWhereCompression => {
1606            ObligationFamilyIdV0::SelectorSpecificityAndCascade
1607        }
1608        TransformPassKind::ShorthandCombining => {
1609            ObligationFamilyIdV0::LonghandShorthandCascadeOutcome
1610        }
1611        TransformPassKind::RuleDeduplication => ObligationFamilyIdV0::DeclarationCascadeOrder,
1612        TransformPassKind::RuleMerging => ObligationFamilyIdV0::RuleMergeWinnerOrder,
1613        TransformPassKind::SelectorMerging => {
1614            ObligationFamilyIdV0::SelectorIdentityAndModuleSemantics
1615        }
1616        TransformPassKind::EmptyRuleRemoval => ObligationFamilyIdV0::SemanticMarkerRetention,
1617        TransformPassKind::VendorPrefixing => ObligationFamilyIdV0::TargetPrefixAddition,
1618        TransformPassKind::StalePrefixRemoval => ObligationFamilyIdV0::StalePrefixRemovalMapping,
1619        TransformPassKind::LightDarkLowering => ObligationFamilyIdV0::TargetFallbackBranch,
1620        TransformPassKind::ColorMixLowering => ObligationFamilyIdV0::ColorSpaceTargetEquivalence,
1621        TransformPassKind::OklchOklabLowering
1622        | TransformPassKind::ColorFunctionLowering
1623        | TransformPassKind::RelativeColorLowering => ObligationFamilyIdV0::TargetColorPrecision,
1624        TransformPassKind::LogicalToPhysical => ObligationFamilyIdV0::DirectionalityOption,
1625        TransformPassKind::NestingUnwrap => ObligationFamilyIdV0::NestedSelectorSpecificity,
1626        TransformPassKind::ScopeFlatten => ObligationFamilyIdV0::ScopedMatching,
1627        TransformPassKind::LayerFlatten => ObligationFamilyIdV0::LayerOrderComparison,
1628        TransformPassKind::SupportsStaticEval => ObligationFamilyIdV0::TargetFeaturePredicate,
1629        TransformPassKind::MediaStaticEval => ObligationFamilyIdV0::MediaPredicate,
1630        TransformPassKind::ContainerStaticEval => ObligationFamilyIdV0::ContainerPredicate,
1631        TransformPassKind::NativeCssStaticEval => ObligationFamilyIdV0::NativeCssStaticValue,
1632        TransformPassKind::CalcReduction => ObligationFamilyIdV0::CalcExpressionEquivalence,
1633        TransformPassKind::ImportInline => ObligationFamilyIdV0::ImportWrapperProvenance,
1634        TransformPassKind::ScssModuleEvaluate => ObligationFamilyIdV0::ScssNamespaceProvenance,
1635        TransformPassKind::LessModuleEvaluate => ObligationFamilyIdV0::LessNamespaceProvenance,
1636        TransformPassKind::HashCssModuleClassNames => ObligationFamilyIdV0::SelectorIdentityMap,
1637        TransformPassKind::ResolveCssModulesComposes => {
1638            ObligationFamilyIdV0::ComposedClassProvenance
1639        }
1640        TransformPassKind::ValueResolution => ObligationFamilyIdV0::ValueGraphResolution,
1641        TransformPassKind::StaticVarSubstitution => ObligationFamilyIdV0::CustomPropertyFixedPoint,
1642        TransformPassKind::TreeShakeClass => ObligationFamilyIdV0::SourceClassReachability,
1643        TransformPassKind::TreeShakeKeyframes => ObligationFamilyIdV0::AnimationNameReachability,
1644        TransformPassKind::TreeShakeValue => ObligationFamilyIdV0::ValueGraphReachability,
1645        TransformPassKind::TreeShakeCustomProperty => ObligationFamilyIdV0::VarReachability,
1646        TransformPassKind::DeadMediaBranchRemoval => ObligationFamilyIdV0::DeadMediaWitness,
1647        TransformPassKind::DeadSupportsBranchRemoval => ObligationFamilyIdV0::DeadSupportsWitness,
1648        TransformPassKind::DesignTokenRouting => ObligationFamilyIdV0::DesignTokenPackageProvenance,
1649        TransformPassKind::PrintCss => ObligationFamilyIdV0::SourceMapTransformTrace,
1650    }
1651}
1652
1653pub const fn cascade_safe_obligation(kind: TransformPassKind) -> &'static str {
1654    obligation_family_for_transform_pass(kind)
1655        .descriptor()
1656        .obligation
1657}
1658
1659#[cfg(test)]
1660fn cascade_safe_obligation_reference(kind: TransformPassKind) -> &'static str {
1661    match kind {
1662        TransformPassKind::WhitespaceStrip => {
1663            "may remove only whitespace outside string, url, attr, and calc-sensitive token boundaries"
1664        }
1665        TransformPassKind::CommentStrip => {
1666            "may remove comments only when source-map provenance preserves the removed span"
1667        }
1668        TransformPassKind::NumberCompression => {
1669            "may rewrite only numerically equivalent literal tokens"
1670        }
1671        TransformPassKind::UnitNormalization => {
1672            "may normalize only dimension values whose computed value is unchanged"
1673        }
1674        TransformPassKind::ColorCompression => "may rewrite only color-equivalent literal tokens",
1675        TransformPassKind::UrlQuoteStrip => {
1676            "may remove url quotes only when the unquoted token grammar remains equivalent"
1677        }
1678        TransformPassKind::StringQuoteNormalize => {
1679            "may normalize string quotes and font keyword aliases only when computed text and font values remain equivalent"
1680        }
1681        TransformPassKind::SelectorIsWhereCompression => {
1682            "must preserve selector specificity, keyframe timeline positions, and matching semantics under the cascade model"
1683        }
1684        TransformPassKind::ShorthandCombining => {
1685            "must prove longhand and shorthand cascade outcomes are equivalent"
1686        }
1687        TransformPassKind::RuleDeduplication => {
1688            "must preserve origin, layer, specificity, and order for every surviving declaration"
1689        }
1690        TransformPassKind::RuleMerging => {
1691            "must prove merged rule order cannot change declaration winners"
1692        }
1693        TransformPassKind::SelectorMerging => {
1694            "must preserve selector identity and post-hash module semantics"
1695        }
1696        TransformPassKind::EmptyRuleRemoval => {
1697            "may remove rules only when no source-visible semantic marker is attached"
1698        }
1699        TransformPassKind::VendorPrefixing => {
1700            "must add target-required prefixed declarations without changing modern target outcomes"
1701        }
1702        TransformPassKind::StalePrefixRemoval => {
1703            "may remove prefixed declarations only when an explicit mapping and exact unprefixed peer prove the prefix stale"
1704        }
1705        TransformPassKind::LightDarkLowering => {
1706            "must lower only when target data requires fallback branches and provenance tracks both branches"
1707        }
1708        TransformPassKind::ColorMixLowering => {
1709            "must lower only when color-space conversion is target-equivalent"
1710        }
1711        TransformPassKind::OklchOklabLowering => {
1712            "must preserve color semantics within the configured target fallback precision"
1713        }
1714        TransformPassKind::ColorFunctionLowering => {
1715            "must preserve color semantics within the configured target fallback precision"
1716        }
1717        TransformPassKind::RelativeColorLowering => {
1718            "must preserve color semantics within the configured target fallback precision"
1719        }
1720        TransformPassKind::LogicalToPhysical => {
1721            "must run only under explicit directionality options"
1722        }
1723        TransformPassKind::NestingUnwrap => {
1724            "must preserve nested selector expansion and specificity"
1725        }
1726        TransformPassKind::ScopeFlatten => {
1727            "must preserve scoped matching semantics or emit a blocked result"
1728        }
1729        TransformPassKind::LayerFlatten => "must preserve layer order in CascadeKey comparison",
1730        TransformPassKind::SupportsStaticEval => {
1731            "may remove branches only when the target feature predicate is known"
1732        }
1733        TransformPassKind::MediaStaticEval => {
1734            "may remove branches only when the configured media predicate is known"
1735        }
1736        TransformPassKind::ContainerStaticEval => {
1737            "may remove @container branches only when the size condition is provably unsatisfiable regardless of container context"
1738        }
1739        TransformPassKind::NativeCssStaticEval => {
1740            "may fold native CSS if() and function calls only when the evaluator proves a concrete static value and preserves runtime-dependent constructs verbatim"
1741        }
1742        TransformPassKind::CalcReduction => {
1743            "may reduce only syntax-equivalent or computed-value-equivalent calc expressions"
1744        }
1745        TransformPassKind::ImportInline => {
1746            "must preserve import-site media, supports, layer wrappers, and source provenance"
1747        }
1748        TransformPassKind::ScssModuleEvaluate => {
1749            "must preserve SCSS namespace, show/hide, mixin, variable, and source provenance facts"
1750        }
1751        TransformPassKind::LessModuleEvaluate => {
1752            "must preserve Less variable, mixin, namespace, and source provenance facts"
1753        }
1754        TransformPassKind::HashCssModuleClassNames => {
1755            "must rewrite every source and style reference through the same selector identity map"
1756        }
1757        TransformPassKind::ResolveCssModulesComposes => {
1758            "must preserve exported class set and composed class provenance"
1759        }
1760        TransformPassKind::ValueResolution => {
1761            "must preserve @value graph resolution and cycle diagnostics"
1762        }
1763        TransformPassKind::StaticVarSubstitution => {
1764            "must preserve custom-property fixed-point semantics or emit a provenance-backed blocked result"
1765        }
1766        TransformPassKind::TreeShakeClass => {
1767            "may remove classes only when bridge reachability proves no reachable source expression observes them"
1768        }
1769        TransformPassKind::TreeShakeKeyframes => {
1770            "may remove keyframes only when animation-name reachability proves they are unobservable"
1771        }
1772        TransformPassKind::TreeShakeValue => {
1773            "may remove @value declarations only when value-graph traversal proves they are unreachable"
1774        }
1775        TransformPassKind::TreeShakeCustomProperty => {
1776            "may remove custom properties only when var() reachability proves they are unobservable"
1777        }
1778        TransformPassKind::DeadMediaBranchRemoval => {
1779            "may remove @media branches only when target and cascade witnesses prove deadness"
1780        }
1781        TransformPassKind::DeadSupportsBranchRemoval => {
1782            "may remove @supports branches only when target and cascade witnesses prove deadness"
1783        }
1784        TransformPassKind::DesignTokenRouting => {
1785            "must preserve design-token provenance while routing declarations across package boundaries"
1786        }
1787        TransformPassKind::PrintCss => {
1788            "must emit a source-map trace for every non-trivia transformed span"
1789        }
1790    }
1791}
1792
1793fn push_ir_node(
1794    nodes: &mut Vec<StableTransformIrNodeV0>,
1795    kind: StableTransformIrNodeKindV0,
1796    label: impl Into<String>,
1797    source_span_start: usize,
1798    source_span_end: usize,
1799) {
1800    let label = label.into();
1801    let kind_id = kind.id();
1802    nodes.push(StableTransformIrNodeV0 {
1803        node_id: String::new(),
1804        node_key: OnceLock::new(),
1805        node_key_u64: OnceLock::new(),
1806        node_key_seed: None,
1807        kind,
1808        kind_id,
1809        semantic_key: format!("{kind_id}:{label}"),
1810        label,
1811        source_span_start,
1812        source_span_end,
1813        provenance_anchor_index: 0,
1814    });
1815}
1816
1817const fn stable_ir_selector_kind(kind: ParsedSelectorFactKind) -> StableTransformIrNodeKindV0 {
1818    match kind {
1819        ParsedSelectorFactKind::Class => StableTransformIrNodeKindV0::ClassSelector,
1820        ParsedSelectorFactKind::Id => StableTransformIrNodeKindV0::IdSelector,
1821        ParsedSelectorFactKind::Placeholder => StableTransformIrNodeKindV0::PlaceholderSelector,
1822    }
1823}
1824
1825const fn stable_ir_variable_kind(kind: ParsedVariableFactKind) -> StableTransformIrNodeKindV0 {
1826    match kind {
1827        ParsedVariableFactKind::ScssDeclaration => {
1828            StableTransformIrNodeKindV0::ScssVariableDeclaration
1829        }
1830        ParsedVariableFactKind::ScssReference => StableTransformIrNodeKindV0::ScssVariableReference,
1831        ParsedVariableFactKind::LessDeclaration => {
1832            StableTransformIrNodeKindV0::LessVariableDeclaration
1833        }
1834        ParsedVariableFactKind::LessReference => StableTransformIrNodeKindV0::LessVariableReference,
1835        ParsedVariableFactKind::CustomPropertyDeclaration => {
1836            StableTransformIrNodeKindV0::CustomPropertyDeclaration
1837        }
1838        ParsedVariableFactKind::CustomPropertyReference => {
1839            StableTransformIrNodeKindV0::CustomPropertyReference
1840        }
1841    }
1842}
1843
1844const fn stable_ir_sass_symbol_kind(kind: ParsedSassSymbolFactKind) -> StableTransformIrNodeKindV0 {
1845    match kind {
1846        ParsedSassSymbolFactKind::VariableDeclaration
1847        | ParsedSassSymbolFactKind::MixinDeclaration
1848        | ParsedSassSymbolFactKind::FunctionDeclaration => {
1849            StableTransformIrNodeKindV0::SassSymbolDeclaration
1850        }
1851        ParsedSassSymbolFactKind::VariableReference
1852        | ParsedSassSymbolFactKind::MixinInclude
1853        | ParsedSassSymbolFactKind::FunctionCall => {
1854            StableTransformIrNodeKindV0::SassSymbolReference
1855        }
1856    }
1857}
1858
1859const fn stable_ir_animation_kind(kind: ParsedAnimationFactKind) -> StableTransformIrNodeKindV0 {
1860    match kind {
1861        ParsedAnimationFactKind::KeyframesDeclaration => {
1862            StableTransformIrNodeKindV0::KeyframesDeclaration
1863        }
1864        ParsedAnimationFactKind::AnimationNameReference => {
1865            StableTransformIrNodeKindV0::AnimationNameReference
1866        }
1867    }
1868}
1869
1870const fn stable_ir_css_module_value_kind(
1871    kind: ParsedCssModuleValueFactKind,
1872) -> StableTransformIrNodeKindV0 {
1873    match kind {
1874        ParsedCssModuleValueFactKind::Definition => {
1875            StableTransformIrNodeKindV0::CssModuleValueDefinition
1876        }
1877        ParsedCssModuleValueFactKind::Reference => {
1878            StableTransformIrNodeKindV0::CssModuleValueReference
1879        }
1880        ParsedCssModuleValueFactKind::ImportSource => {
1881            StableTransformIrNodeKindV0::CssModuleValueImportSource
1882        }
1883    }
1884}
1885
1886const fn stable_ir_css_module_composes_kind(
1887    kind: ParsedCssModuleComposesFactKind,
1888) -> StableTransformIrNodeKindV0 {
1889    match kind {
1890        ParsedCssModuleComposesFactKind::Target => {
1891            StableTransformIrNodeKindV0::CssModuleComposesTarget
1892        }
1893        ParsedCssModuleComposesFactKind::ImportSource => {
1894            StableTransformIrNodeKindV0::CssModuleComposesImportSource
1895        }
1896    }
1897}
1898
1899const fn stable_ir_icss_kind(kind: ParsedIcssFactKind) -> StableTransformIrNodeKindV0 {
1900    match kind {
1901        ParsedIcssFactKind::ExportName => StableTransformIrNodeKindV0::IcssExportName,
1902        ParsedIcssFactKind::ImportLocalName => StableTransformIrNodeKindV0::IcssImportLocalName,
1903        ParsedIcssFactKind::ImportRemoteName => StableTransformIrNodeKindV0::IcssImportRemoteName,
1904        ParsedIcssFactKind::ImportSource => StableTransformIrNodeKindV0::IcssImportSource,
1905    }
1906}
1907
1908pub const fn transform_cst_style_dialect_label(dialect: StyleDialect) -> &'static str {
1909    match dialect {
1910        StyleDialect::Css => "css",
1911        StyleDialect::Scss => "scss",
1912        StyleDialect::Sass => "sass",
1913        StyleDialect::Less => "less",
1914    }
1915}
1916
1917pub fn default_transform_dag_edges() -> Vec<TransformDagEdgeV0> {
1918    vec![
1919        TransformDagEdgeV0 {
1920            from: "import-inline",
1921            to: "custom-property-static-resolve",
1922            reason: "var() resolution needs the full custom-property graph from inlined files",
1923        },
1924        TransformDagEdgeV0 {
1925            from: "scss-module-evaluate",
1926            to: "custom-property-static-resolve",
1927            reason: "SCSS evaluation can introduce custom-property declarations",
1928        },
1929        TransformDagEdgeV0 {
1930            from: "less-module-evaluate",
1931            to: "custom-property-static-resolve",
1932            reason: "Less evaluation can introduce custom-property declarations",
1933        },
1934        TransformDagEdgeV0 {
1935            from: "composes-resolution",
1936            to: "css-modules-class-hashing",
1937            reason: "hashing must run after composed class expansion",
1938        },
1939        TransformDagEdgeV0 {
1940            from: "nesting-unwrap",
1941            to: "css-modules-class-hashing",
1942            reason: "hashing must run after nested selectors are expanded into final selector branches",
1943        },
1944        TransformDagEdgeV0 {
1945            from: "tree-shake-class",
1946            to: "css-modules-class-hashing",
1947            reason: "class reachability is expressed in authored selector names and must run before hashing rewrites them",
1948        },
1949        TransformDagEdgeV0 {
1950            from: "css-modules-class-hashing",
1951            to: "selector-merging",
1952            reason: "selector merging must see post-hash selector identities",
1953        },
1954        TransformDagEdgeV0 {
1955            from: "number-compression",
1956            to: "selector-merging",
1957            reason: "selector merging must see canonical declaration numeric values",
1958        },
1959        TransformDagEdgeV0 {
1960            from: "unit-normalization",
1961            to: "selector-merging",
1962            reason: "selector merging must see canonical declaration unit values",
1963        },
1964        TransformDagEdgeV0 {
1965            from: "color-compression",
1966            to: "selector-merging",
1967            reason: "selector merging must see canonical declaration color values",
1968        },
1969        TransformDagEdgeV0 {
1970            from: "url-quote-strip",
1971            to: "selector-merging",
1972            reason: "selector merging must see canonical url() values",
1973        },
1974        TransformDagEdgeV0 {
1975            from: "string-quote-normalize",
1976            to: "selector-merging",
1977            reason: "selector merging must see canonical string values",
1978        },
1979        TransformDagEdgeV0 {
1980            from: "shorthand-combining",
1981            to: "selector-merging",
1982            reason: "selector merging must see canonical shorthand declaration blocks",
1983        },
1984        TransformDagEdgeV0 {
1985            from: "shorthand-combining",
1986            to: "rule-merging",
1987            reason: "rule merging must see shorthand-combined declaration blocks before comparing adjacent rules",
1988        },
1989        TransformDagEdgeV0 {
1990            from: "calc-reduction",
1991            to: "selector-merging",
1992            reason: "selector merging must see reduced calc() declaration values",
1993        },
1994        TransformDagEdgeV0 {
1995            from: "selector-merging",
1996            to: "whitespace-strip",
1997            reason: "whitespace stripping must run after selector merging emits final selector lists",
1998        },
1999        TransformDagEdgeV0 {
2000            from: "custom-property-static-resolve",
2001            to: "calc-reduction",
2002            reason: "var() inside calc may resolve to numeric literals that enable reduction",
2003        },
2004        TransformDagEdgeV0 {
2005            from: "value-resolution",
2006            to: "supports-static-eval",
2007            reason: "@value references inside @supports preludes must resolve before static branch evaluation",
2008        },
2009        TransformDagEdgeV0 {
2010            from: "value-resolution",
2011            to: "media-static-eval",
2012            reason: "@value references inside @media preludes must resolve before static media normalization",
2013        },
2014        TransformDagEdgeV0 {
2015            from: "custom-property-static-resolve",
2016            to: "supports-static-eval",
2017            reason: "var() references inside @supports preludes must resolve before static branch evaluation",
2018        },
2019        TransformDagEdgeV0 {
2020            from: "custom-property-static-resolve",
2021            to: "media-static-eval",
2022            reason: "var() references inside @media preludes must resolve before static media normalization",
2023        },
2024        TransformDagEdgeV0 {
2025            from: "value-resolution",
2026            to: "native-css-static-eval",
2027            reason: "@value references inside native CSS conditional values and function arguments must resolve before static native evaluation",
2028        },
2029        TransformDagEdgeV0 {
2030            from: "custom-property-static-resolve",
2031            to: "native-css-static-eval",
2032            reason: "var() references inside native CSS conditional values and function arguments must resolve before static native evaluation",
2033        },
2034        TransformDagEdgeV0 {
2035            from: "native-css-static-eval",
2036            to: "calc-reduction",
2037            reason: "native CSS static evaluation can expose calc() values that should reduce after folding",
2038        },
2039        TransformDagEdgeV0 {
2040            from: "tree-shake-class",
2041            to: "rule-deduplication",
2042            reason: "tree shaking must run before rule deduplication can hide dead rules",
2043        },
2044        TransformDagEdgeV0 {
2045            from: "tree-shake-keyframes",
2046            to: "rule-deduplication",
2047            reason: "keyframe reachability must settle before rule deduplication",
2048        },
2049        TransformDagEdgeV0 {
2050            from: "tree-shake-value",
2051            to: "rule-deduplication",
2052            reason: "@value reachability must settle before rule deduplication",
2053        },
2054        TransformDagEdgeV0 {
2055            from: "tree-shake-custom-property",
2056            to: "rule-deduplication",
2057            reason: "custom-property reachability must settle before rule deduplication",
2058        },
2059        TransformDagEdgeV0 {
2060            from: "tree-shake-class",
2061            to: "empty-rule-removal",
2062            reason: "class tree shaking can leave ordinary and group rules empty",
2063        },
2064        TransformDagEdgeV0 {
2065            from: "tree-shake-keyframes",
2066            to: "empty-rule-removal",
2067            reason: "keyframe tree shaking can leave enclosing group rules empty",
2068        },
2069        TransformDagEdgeV0 {
2070            from: "tree-shake-value",
2071            to: "empty-rule-removal",
2072            reason: "@value tree shaking can leave module-only wrappers empty",
2073        },
2074        TransformDagEdgeV0 {
2075            from: "tree-shake-custom-property",
2076            to: "empty-rule-removal",
2077            reason: "custom-property tree shaking can leave declaration-only rules empty",
2078        },
2079        TransformDagEdgeV0 {
2080            from: "comment-strip",
2081            to: "empty-rule-removal",
2082            reason: "comment-only rules become removable empty rules after comment stripping",
2083        },
2084        TransformDagEdgeV0 {
2085            from: "light-dark-lowering",
2086            to: "vendor-prefixing",
2087            reason: "prefixing runs after target lowering produces final declarations",
2088        },
2089        TransformDagEdgeV0 {
2090            from: "color-mix-lowering",
2091            to: "vendor-prefixing",
2092            reason: "prefixing runs after target lowering produces final declarations",
2093        },
2094        TransformDagEdgeV0 {
2095            from: "oklch-oklab-lowering",
2096            to: "vendor-prefixing",
2097            reason: "prefixing runs after target lowering produces final declarations",
2098        },
2099        TransformDagEdgeV0 {
2100            from: "color-function-lowering",
2101            to: "vendor-prefixing",
2102            reason: "prefixing runs after target lowering produces final declarations",
2103        },
2104        TransformDagEdgeV0 {
2105            from: "relative-color-lowering",
2106            to: "vendor-prefixing",
2107            reason: "prefixing runs after target lowering produces final declarations",
2108        },
2109        TransformDagEdgeV0 {
2110            from: "logical-to-physical",
2111            to: "vendor-prefixing",
2112            reason: "prefixing runs after target lowering produces final declarations",
2113        },
2114        TransformDagEdgeV0 {
2115            from: "nesting-unwrap",
2116            to: "vendor-prefixing",
2117            reason: "prefixing runs after target lowering produces final declarations",
2118        },
2119        TransformDagEdgeV0 {
2120            from: "scope-flatten",
2121            to: "vendor-prefixing",
2122            reason: "prefixing runs after target lowering produces final declarations",
2123        },
2124        TransformDagEdgeV0 {
2125            from: "layer-flatten",
2126            to: "vendor-prefixing",
2127            reason: "prefixing runs after target lowering produces final declarations",
2128        },
2129        TransformDagEdgeV0 {
2130            from: "supports-static-eval",
2131            to: "vendor-prefixing",
2132            reason: "prefixing runs after target branch evaluation produces final declarations",
2133        },
2134        TransformDagEdgeV0 {
2135            from: "media-static-eval",
2136            to: "vendor-prefixing",
2137            reason: "prefixing runs after target branch evaluation produces final declarations",
2138        },
2139        TransformDagEdgeV0 {
2140            from: "native-css-static-eval",
2141            to: "vendor-prefixing",
2142            reason: "prefixing runs after native CSS static evaluation produces final declarations",
2143        },
2144        TransformDagEdgeV0 {
2145            from: "vendor-prefixing",
2146            to: "stale-prefix-removal",
2147            reason: "stale-prefix removal must inspect the final vendor-prefix declaration set",
2148        },
2149        TransformDagEdgeV0 {
2150            from: "stale-prefix-removal",
2151            to: "print-css",
2152            reason: "printer consumes the final prefix-removal decisions",
2153        },
2154        TransformDagEdgeV0 {
2155            from: "calc-reduction",
2156            to: "print-css",
2157            reason: "printer consumes the final reduced transform CST",
2158        },
2159        TransformDagEdgeV0 {
2160            from: "whitespace-strip",
2161            to: "print-css",
2162            reason: "printer consumes the final trivia policy",
2163        },
2164    ]
2165}
2166
2167#[cfg(test)]
2168mod tests {
2169    use super::{
2170        NATIVE_CSS_STATIC_EVAL_DIALECT_RESTRICTION_V0, NATIVE_CSS_STATIC_EVAL_OPT_IN_POLICY_V0,
2171        NATIVE_CSS_STATIC_EVAL_SPEC_SNAPSHOT_V0, ObservationKindV0, PassObservationSurfaceV0,
2172        RewriteCandidateV0, STABLE_TRANSFORM_IR_NODE_IDENTITY_POLICY_V0,
2173        StableTransformIrNodeKindV0, StyleDialect, TRANSFORM_PASS_CATALOG_LEN, TransformLayer,
2174        TransformPassClassV0, TransformPassKind, TransformVerificationErrorV0,
2175        all_transform_pass_kinds, apply_verified_rewrite, build_stable_transform_ir_from_source,
2176        build_transform_cst_artifact, build_verified_transform_cst_artifact_with_dialect,
2177        cascade_safe_obligation, cascade_safe_obligation_reference, cascade_safety_witness,
2178        default_transform_pass_descriptors, obligation_family_for_transform_pass,
2179        pass_observation_contract, summarize_omena_transform_cst_boundary,
2180        transform_build_profile_from_passes, verify_rewrite_candidate,
2181        verify_rewrite_candidate_with_backend, verify_rewrite_candidate_with_closed_world_bundle,
2182    };
2183    use omena_cascade_proof::{
2184        CanonicalSmtInputV0, SMT_FEATURE_GATE_V0, SMT_LAYER_MARKER_V0, SMT_SCHEMA_VERSION_V0,
2185        SmtBackendCheckV0, SmtBackendKindV0, SmtBackendSatResultV0, SmtBackendV0, SmtVerdictV0,
2186    };
2187    use omena_evidence_graph::{GuaranteeFamilyV0, GuaranteeKindV0};
2188    use omena_parser::{
2189        ClosedWorldBundleV0, ClosedWorldLinkedModuleV0, ConfigurationHashV0, ModuleIdV0,
2190        ModuleInstanceKeyV0,
2191    };
2192
2193    struct RejectingBackend;
2194
2195    impl SmtBackendV0 for RejectingBackend {
2196        fn backend_kind(&self) -> SmtBackendKindV0 {
2197            SmtBackendKindV0::Stub
2198        }
2199
2200        fn check_canonical_input_v0(&self, input: &CanonicalSmtInputV0) -> SmtBackendCheckV0 {
2201            SmtBackendCheckV0 {
2202                schema_version: SMT_SCHEMA_VERSION_V0,
2203                product: "omena-smt.backend-check",
2204                layer_marker: SMT_LAYER_MARKER_V0,
2205                feature_gate: SMT_FEATURE_GATE_V0,
2206                backend: self.backend_kind(),
2207                obligation_id: input.obligation_id.clone(),
2208                formula_count: input.canonical_terms.len(),
2209                sat_result: SmtBackendSatResultV0::Unsat,
2210                model_available: false,
2211            }
2212        }
2213    }
2214
2215    #[test]
2216    fn exposes_transform_cst_boundary_with_full_pass_catalog() {
2217        let boundary = summarize_omena_transform_cst_boundary();
2218
2219        assert_eq!(boundary.schema_version, "0");
2220        assert_eq!(boundary.product, "omena-transform-cst.boundary");
2221        assert_eq!(boundary.pass_catalog_count, TRANSFORM_PASS_CATALOG_LEN);
2222        assert_eq!(
2223            boundary.pass_observation_record_count,
2224            TRANSFORM_PASS_CATALOG_LEN
2225        );
2226        assert!(boundary.full_pass_catalog_covered);
2227        assert!(boundary.all_passes_have_observation_surface);
2228        assert!(boundary.all_observation_gaps_are_reasoned);
2229        assert_eq!(boundary.semantic_aware_pass_count, 14);
2230        assert_eq!(boundary.commodity_pass_count, 29);
2231        assert_eq!(boundary.emission_pass_count, 1);
2232        assert_eq!(boundary.pass_descriptors.len(), TRANSFORM_PASS_CATALOG_LEN);
2233        assert_eq!(boundary.structural_pass_count, 21);
2234        assert_eq!(boundary.text_local_pass_count, 20);
2235        assert_eq!(boundary.module_evaluation_pass_count, 2);
2236        assert!(boundary.all_passes_declare_cascade_obligation);
2237        assert!(boundary.all_passes_have_compile_time_cascade_witness);
2238        assert!(boundary.stable_transform_ir_ready);
2239        assert!(boundary.provenance_derivation_forest_scaffold_ready);
2240        assert!(boundary.provenance_preservation_required);
2241        assert!(!boundary.next_surfaces.contains(&"omena-transform-passes"));
2242        assert!(!boundary.next_surfaces.contains(&"omena-transform-print"));
2243        assert!(!boundary.next_surfaces.contains(&"salsaTransformQueries"));
2244        assert!(!boundary.next_surfaces.contains(&"sourceMapSpanPrecision"));
2245        assert!(boundary.pass_contracts.iter().any(|contract| {
2246            contract.kind == TransformPassKind::TreeShakeClass
2247                && contract.label == "tree-shake-class"
2248                && contract.layer == TransformLayer::SemanticAware
2249                && contract.reads_semantic_graph
2250                && !contract.cascade_obligation.is_empty()
2251                && contract.cascade_safety_witness.pass_id == "tree-shake-class"
2252                && contract.cascade_safety_witness.obligation == contract.cascade_obligation
2253                && contract.cascade_safety_witness.enforced_at
2254                    == "compile-time-exhaustive-pass-catalog"
2255        }));
2256        assert!(boundary.pass_contracts.iter().any(|contract| {
2257            contract.kind == TransformPassKind::NativeCssStaticEval
2258                && contract.label == "native-css-static-eval"
2259                && contract.layer == TransformLayer::Commodity
2260                && contract.read_model == super::TransformPassReadModel::TargetData
2261                && contract.cascade_safety_witness.pass_id == "native-css-static-eval"
2262                && contract.explicit_opt_in_required
2263                && contract.dialect_restriction
2264                    == Some(NATIVE_CSS_STATIC_EVAL_DIALECT_RESTRICTION_V0)
2265                && contract.spec_snapshot == Some(NATIVE_CSS_STATIC_EVAL_SPEC_SNAPSHOT_V0)
2266                && contract.opt_in_policy == Some(NATIVE_CSS_STATIC_EVAL_OPT_IN_POLICY_V0)
2267        }));
2268        assert!(boundary.dag_edges.iter().any(|edge| {
2269            edge.from == "composes-resolution" && edge.to == "css-modules-class-hashing"
2270        }));
2271        assert!(boundary.pass_descriptors.iter().any(|descriptor| {
2272            descriptor.kind == TransformPassKind::NestingUnwrap
2273                && descriptor.pass_class == TransformPassClassV0::Structural
2274        }));
2275        assert!(boundary.pass_descriptors.iter().any(|descriptor| {
2276            descriptor.kind == TransformPassKind::StaticVarSubstitution
2277                && descriptor.pass_class == TransformPassClassV0::TextLocal
2278        }));
2279        assert!(boundary.pass_descriptors.iter().any(|descriptor| {
2280            descriptor.kind == TransformPassKind::ScssModuleEvaluate
2281                && descriptor.pass_class == TransformPassClassV0::ModuleEvaluation
2282        }));
2283        assert!(boundary.pass_observation_records.iter().any(|record| {
2284            record.kind == TransformPassKind::LayerFlatten
2285                && matches!(&record.surface, PassObservationSurfaceV0::Declared(contract)
2286                    if contract.observes.contains(&ObservationKindV0::LayerRank)
2287                        && contract.preserves.contains(&ObservationKindV0::CascadeWinner))
2288        }));
2289    }
2290
2291    #[test]
2292    fn pass_descriptors_pin_classification_phase_and_dependency_contracts() -> Result<(), String> {
2293        let descriptors = default_transform_pass_descriptors();
2294
2295        assert_eq!(descriptors.len(), TRANSFORM_PASS_CATALOG_LEN);
2296        assert!(
2297            descriptors
2298                .iter()
2299                .all(|descriptor| descriptor.schema_version == "0"
2300                    && descriptor.product == "omena-transform-cst.pass-descriptor"
2301                    && descriptor.id == descriptor.kind.id())
2302        );
2303        assert_eq!(
2304            descriptors
2305                .iter()
2306                .filter(|descriptor| descriptor.pass_class == TransformPassClassV0::Structural)
2307                .count(),
2308            21
2309        );
2310        assert_eq!(
2311            descriptors
2312                .iter()
2313                .filter(|descriptor| descriptor.pass_class == TransformPassClassV0::TextLocal)
2314                .count(),
2315            20
2316        );
2317        assert_eq!(
2318            descriptors
2319                .iter()
2320                .filter(|descriptor| descriptor.pass_class == TransformPassClassV0::ModuleEvaluation)
2321                .count(),
2322            2
2323        );
2324        assert_eq!(
2325            descriptors
2326                .iter()
2327                .filter(|descriptor| descriptor.pass_class == TransformPassClassV0::Emission)
2328                .count(),
2329            1
2330        );
2331
2332        let hash_descriptor = descriptors
2333            .iter()
2334            .find(|descriptor| descriptor.kind == TransformPassKind::HashCssModuleClassNames)
2335            .ok_or_else(|| "missing hash descriptor".to_string())?;
2336        assert_eq!(hash_descriptor.pass_class, TransformPassClassV0::Structural);
2337        assert!(
2338            hash_descriptor.depends_on.contains(&"composes-resolution")
2339                && hash_descriptor.depends_on.contains(&"nesting-unwrap")
2340                && hash_descriptor.depends_on.contains(&"tree-shake-class")
2341        );
2342
2343        let profile = transform_build_profile_from_passes(
2344            "requested-transform-plan",
2345            &[
2346                TransformPassKind::CommentStrip,
2347                TransformPassKind::WhitespaceStrip,
2348            ],
2349        );
2350        assert_eq!(profile.schema_version, "0");
2351        assert_eq!(profile.profile_id, "requested-transform-plan");
2352        assert_eq!(profile.pass_ids, vec!["comment-strip", "whitespace-strip"]);
2353        assert_ne!(profile.pass_ids.len(), TRANSFORM_PASS_CATALOG_LEN);
2354        Ok(())
2355    }
2356
2357    #[test]
2358    fn pass_observation_contracts_cover_the_transform_catalog() {
2359        let records = super::default_transform_pass_observation_records();
2360
2361        assert_eq!(records.len(), TRANSFORM_PASS_CATALOG_LEN);
2362        assert!(records.iter().all(|record| record.id == record.kind.id()));
2363        assert!(records.iter().all(|record| {
2364            record.surface.is_declared()
2365                || record
2366                    .surface
2367                    .gap_reason()
2368                    .is_some_and(|reason| !reason.is_empty())
2369        }));
2370
2371        for kind in all_transform_pass_kinds() {
2372            assert!(records.iter().any(|record| record.kind == kind));
2373        }
2374
2375        let keyframes = pass_observation_contract(TransformPassKind::TreeShakeKeyframes);
2376        assert!(
2377            matches!(keyframes, PassObservationSurfaceV0::Declared(contract)
2378            if contract.observes.contains(&ObservationKindV0::KeyframesReachability)
2379                && contract.preserves.contains(&ObservationKindV0::KeyframesReachability))
2380        );
2381
2382        let print = pass_observation_contract(TransformPassKind::PrintCss);
2383        assert!(matches!(print, PassObservationSurfaceV0::Declared(contract)
2384            if contract.observes.contains(&ObservationKindV0::SourceMapTrace)
2385                && contract.preserves.contains(&ObservationKindV0::SourceMapTrace)));
2386    }
2387
2388    #[test]
2389    fn transform_cst_artifact_preserves_semantic_signature_and_pass_ids() {
2390        let artifact = build_transform_cst_artifact(
2391            ".button { color: var(--brand); }",
2392            "semantic:button:brand",
2393            &[
2394                TransformPassKind::StaticVarSubstitution,
2395                TransformPassKind::ColorCompression,
2396            ],
2397        );
2398
2399        assert_eq!(artifact.product, "omena-transform-cst.artifact");
2400        assert_eq!(artifact.source_byte_len, 32);
2401        assert_eq!(artifact.semantic_signature, "semantic:button:brand");
2402        assert_eq!(artifact.stable_ir.product, "omena-transform-cst.stable-ir");
2403        assert_eq!(artifact.stable_ir.dialect, "css");
2404        assert_eq!(artifact.parser_error_count, 0);
2405        assert!(!artifact.contains_bogus_or_trivia);
2406        assert!(artifact.stable_ir.stable_post_semantic_ir);
2407        assert_eq!(
2408            artifact.stable_ir_node_count,
2409            artifact.stable_ir.provenance_anchors.len()
2410        );
2411        assert_eq!(
2412            artifact.pass_ids,
2413            vec!["custom-property-static-resolve", "color-compression"]
2414        );
2415        assert!(artifact.provenance_preserved);
2416    }
2417
2418    #[test]
2419    fn verified_rewrite_requires_accepted_cascade_proof() -> Result<(), String> {
2420        let candidate = RewriteCandidateV0::from_sources(
2421            TransformPassKind::RuleDeduplication,
2422            ".button { color: red; }",
2423            ".button { color: red; }",
2424            StyleDialect::Css,
2425            "semantic:button",
2426        );
2427        let err = match verify_rewrite_candidate_with_backend(candidate, &RejectingBackend) {
2428            Ok(_) => {
2429                return Err(
2430                    "rejecting backend must prevent verified rewrite construction".to_string(),
2431                );
2432            }
2433            Err(err) => err,
2434        };
2435
2436        assert_eq!(
2437            err,
2438            TransformVerificationErrorV0::CascadeProofRejected {
2439                pass_id: "rule-deduplication",
2440                verdict: SmtVerdictV0::Rejected,
2441            }
2442        );
2443        Ok(())
2444    }
2445
2446    #[test]
2447    fn verified_rewrite_token_is_the_artifact_apply_input() -> Result<(), String> {
2448        let candidate = RewriteCandidateV0::from_sources(
2449            TransformPassKind::ColorCompression,
2450            ".button { color: #ffffff; }",
2451            ".button { color: #ffffff; }",
2452            StyleDialect::Css,
2453            "semantic:button",
2454        );
2455        let verified = match verify_rewrite_candidate(candidate) {
2456            Ok(verified) => verified,
2457            Err(err) => {
2458                return Err(format!(
2459                    "default proof backend should accept recomputed stable IR: {err:?}"
2460                ));
2461            }
2462        };
2463        let artifact = apply_verified_rewrite(&verified);
2464
2465        assert!(verified.verification_report().provenance_preserved());
2466        assert!(verified.verification_report().cascade_safe());
2467        assert_eq!(
2468            verified.verification_report().cascade_proof().verdict,
2469            SmtVerdictV0::Accepted
2470        );
2471        assert_eq!(artifact.pass_ids, vec!["color-compression"]);
2472        assert_eq!(artifact.source_byte_len, 27);
2473        assert!(artifact.provenance_preserved);
2474        assert!(!artifact.contains_bogus_or_trivia);
2475        Ok(())
2476    }
2477
2478    #[test]
2479    fn verified_rewrite_requires_closed_world_bundle_for_reachability_pass() -> Result<(), String> {
2480        let candidate = RewriteCandidateV0::from_sources(
2481            TransformPassKind::TreeShakeClass,
2482            ".used { color: red; }",
2483            ".used { color: red; }",
2484            StyleDialect::Css,
2485            "semantic:used",
2486        );
2487        let err = verify_rewrite_candidate(candidate.clone());
2488        assert_eq!(
2489            err,
2490            Err(TransformVerificationErrorV0::ClosedWorldBundleRequired {
2491                pass_id: "tree-shake-class"
2492            })
2493        );
2494
2495        let instance = ModuleInstanceKeyV0::new(
2496            ModuleIdV0::new("verified-rewrite.css"),
2497            ConfigurationHashV0::none(),
2498        );
2499        let bundle = ClosedWorldBundleV0::try_from_linked_modules(
2500            vec![instance.clone()],
2501            vec![ClosedWorldLinkedModuleV0::new(instance).with_class_name("used")],
2502        )
2503        .map_err(|err| format!("closed-world bundle should be constructible: {err:?}"))?;
2504        let verified = verify_rewrite_candidate_with_closed_world_bundle(candidate, &bundle)
2505            .map_err(|err| format!("bundle-backed rewrite should verify: {err:?}"))?;
2506
2507        assert_eq!(
2508            verified.verification_report().closed_world_bundle_hash(),
2509            Some(bundle.closure_hash())
2510        );
2511
2512        let open_candidate = RewriteCandidateV0::from_sources(
2513            TransformPassKind::ColorCompression,
2514            ".button { color: #ffffff; }",
2515            ".button { color: #fff; }",
2516            StyleDialect::Css,
2517            "semantic:button",
2518        );
2519        let open_verified = verify_rewrite_candidate(open_candidate)
2520            .map_err(|err| format!("open rewrite should stay bundle-free: {err:?}"))?;
2521        assert_eq!(
2522            open_verified
2523                .verification_report()
2524                .closed_world_bundle_hash(),
2525            None
2526        );
2527        Ok(())
2528    }
2529
2530    #[test]
2531    fn verified_artifact_builder_routes_through_typestate_report() -> Result<(), String> {
2532        let artifact = match build_verified_transform_cst_artifact_with_dialect(
2533            ".button { color: red; }",
2534            StyleDialect::Css,
2535            "semantic:button",
2536            &[
2537                TransformPassKind::RuleDeduplication,
2538                TransformPassKind::ColorCompression,
2539            ],
2540        ) {
2541            Ok(artifact) => artifact,
2542            Err(err) => {
2543                return Err(format!(
2544                    "valid stable IR should produce verified artifact: {err:?}"
2545                ));
2546            }
2547        };
2548
2549        assert_eq!(
2550            artifact.pass_ids,
2551            vec!["rule-deduplication", "color-compression"]
2552        );
2553        assert!(artifact.provenance_preserved);
2554        Ok(())
2555    }
2556
2557    #[test]
2558    fn transform_cst_boolean_fields_are_not_direct_literal_assignments() -> Result<(), String> {
2559        let source = match std::fs::read_to_string(
2560            std::path::Path::new(env!("CARGO_MANIFEST_DIR"))
2561                .join("src")
2562                .join("lib.rs"),
2563        ) {
2564            Ok(source) => source,
2565            Err(err) => return Err(format!("test source should be readable: {err:?}")),
2566        };
2567        for forbidden in [
2568            ["cascade_safe", ": true"].concat(),
2569            ["provenance_preserved", ": true"].concat(),
2570            ["contains_bogus_or_trivia", ": false"].concat(),
2571        ] {
2572            assert!(
2573                !source.contains(&forbidden),
2574                "{forbidden} must be derived through verification instead of assigned directly"
2575            );
2576        }
2577        assert!(
2578            !source.contains(&["pub ", "cascade_safe", ": bool"].concat()),
2579            "pass contracts must not expose static cascade safety as a catalog field"
2580        );
2581        Ok(())
2582    }
2583
2584    #[test]
2585    fn stable_transform_ir_consumes_parser_semantic_facts_without_trivia_or_bogus_nodes() {
2586        let ir = build_stable_transform_ir_from_source(
2587            r#"
2588@use "./tokens" as tokens;
2589@value primary from "./colors.module.css";
2590.button {
2591  composes: reset from "./reset.module.css";
2592  --brand: tokens.$brand;
2593  color: var(--brand);
2594}
2595"#,
2596            StyleDialect::Scss,
2597            "semantic:scss-button",
2598        );
2599
2600        assert_eq!(ir.product, "omena-transform-cst.stable-ir");
2601        assert_eq!(ir.dialect, "scss");
2602        assert_eq!(ir.parser_error_count, 0);
2603        assert!(!ir.contains_bogus_or_trivia);
2604        assert!(ir.stable_post_semantic_ir);
2605        assert_eq!(ir.node_count, ir.nodes.len());
2606        assert_eq!(ir.node_count, ir.provenance_anchors.len());
2607        assert!(ir.nodes.iter().any(|node| {
2608            node.kind == StableTransformIrNodeKindV0::ClassSelector && node.label == "button"
2609        }));
2610        assert!(ir.nodes.iter().any(|node| {
2611            node.kind == StableTransformIrNodeKindV0::CustomPropertyDeclaration
2612                && node.label == "--brand"
2613        }));
2614        assert!(ir.nodes.iter().any(|node| {
2615            node.kind == StableTransformIrNodeKindV0::CustomPropertyReference
2616                && node.label == "--brand"
2617        }));
2618        assert!(ir.nodes.iter().any(|node| {
2619            node.kind == StableTransformIrNodeKindV0::SassModuleEdge && node.label == "./tokens"
2620        }));
2621        assert!(
2622            ir.nodes
2623                .windows(2)
2624                .all(|pair| pair[0].source_span_start <= pair[1].source_span_start)
2625        );
2626    }
2627
2628    #[test]
2629    fn stable_transform_ir_lazily_materializes_source_order_node_keys() {
2630        super::reset_stable_node_key_stamp_count_for_test();
2631        let ir = build_stable_transform_ir_from_source(
2632            ".button { color: red; }\n.button { color: blue; }",
2633            StyleDialect::Css,
2634            "semantic:duplicate-button",
2635        );
2636        assert_eq!(super::stable_node_key_stamp_count_for_test(), 0);
2637
2638        let button_nodes = ir
2639            .nodes
2640            .iter()
2641            .filter(|node| {
2642                node.kind == StableTransformIrNodeKindV0::ClassSelector && node.label == "button"
2643            })
2644            .collect::<Vec<_>>();
2645
2646        assert_eq!(button_nodes.len(), 2);
2647        assert_eq!(button_nodes[0].node_id, "ir:0");
2648        assert_eq!(button_nodes[1].node_id, "ir:1");
2649        assert_eq!(
2650            button_nodes[0].additive_node_key().map(|key| key.as_str()),
2651            Some("class-selector:button#0")
2652        );
2653        assert_eq!(super::stable_node_key_stamp_count_for_test(), 1);
2654        assert_eq!(
2655            button_nodes[0].additive_node_key().map(|key| key.as_str()),
2656            Some("class-selector:button#0")
2657        );
2658        assert_eq!(super::stable_node_key_stamp_count_for_test(), 1);
2659        assert_eq!(
2660            button_nodes[1].additive_node_key().map(|key| key.as_str()),
2661            Some("class-selector:button#1")
2662        );
2663        assert!(button_nodes[0].additive_node_key_u64() != button_nodes[1].additive_node_key_u64());
2664        assert!(ir.nodes.iter().enumerate().all(|(index, node)| {
2665            node.node_id == format!("ir:{index}") && node.additive_node_key().is_some()
2666        }));
2667    }
2668
2669    #[test]
2670    fn stable_transform_ir_u64_keys_preserve_string_key_equivalence_classes() {
2671        let ir = build_stable_transform_ir_from_source(
2672            ".button { color: red; }\n.button { color: blue; }\n.card { color: red; }",
2673            StyleDialect::Css,
2674            "semantic:key-equivalence",
2675        );
2676        let keyed_nodes = ir
2677            .nodes
2678            .iter()
2679            .map(|node| {
2680                (
2681                    node.semantic_key.as_str(),
2682                    node.additive_node_key()
2683                        .map(|key| key.as_str())
2684                        .unwrap_or_default(),
2685                    node.additive_node_key_u64()
2686                        .map(|key| key.as_u64())
2687                        .unwrap_or_default(),
2688                )
2689            })
2690            .collect::<Vec<_>>();
2691
2692        assert!(
2693            keyed_nodes
2694                .iter()
2695                .filter(|(semantic_key, _, _)| *semantic_key == "class-selector:button")
2696                .count()
2697                >= 2
2698        );
2699        for (left_index, (_, left_string, left_u64)) in keyed_nodes.iter().enumerate() {
2700            for (_, right_string, right_u64) in keyed_nodes.iter().skip(left_index + 1) {
2701                assert_eq!(left_string == right_string, left_u64 == right_u64);
2702            }
2703        }
2704        let button_keys = keyed_nodes
2705            .iter()
2706            .filter(|(semantic_key, _, _)| *semantic_key == "class-selector:button")
2707            .map(|(_, string_key, u64_key)| (*string_key, *u64_key))
2708            .collect::<Vec<_>>();
2709        assert_ne!(button_keys[0].0, button_keys[1].0);
2710        assert_ne!(button_keys[0].1, button_keys[1].1);
2711    }
2712
2713    #[test]
2714    fn stable_transform_ir_identity_reader_prefers_key_with_positional_fallback() {
2715        let mut ir = build_stable_transform_ir_from_source(
2716            ".button { color: red; }\n.button { color: blue; }",
2717            StyleDialect::Css,
2718            "semantic:duplicate-button",
2719        );
2720
2721        assert_eq!(
2722            ir.node_identity_policy(),
2723            STABLE_TRANSFORM_IR_NODE_IDENTITY_POLICY_V0
2724        );
2725        assert_eq!(
2726            ir.identity_key_at(0).as_deref(),
2727            Some("class-selector:button#0")
2728        );
2729        assert_eq!(
2730            ir.identity_key_at(1).as_deref(),
2731            Some("class-selector:button#1")
2732        );
2733
2734        ir.nodes[0].clear_additive_node_key_for_test();
2735        assert_eq!(ir.identity_key_at(0).as_deref(), Some("ir:0"));
2736        assert_eq!(
2737            ir.identity_key_at(1).as_deref(),
2738            Some("class-selector:button#1")
2739        );
2740
2741        ir.schema_version = "future";
2742        assert_eq!(ir.node_identity_policy(), "legacy-node-id-only");
2743        assert_eq!(ir.identity_key_at(1).as_deref(), Some("ir:1"));
2744    }
2745
2746    #[test]
2747    fn stable_transform_ir_identity_reader_preserves_serialized_node_shape()
2748    -> Result<(), serde_json::Error> {
2749        let mut ir = build_stable_transform_ir_from_source(
2750            ".button { color: red; }",
2751            StyleDialect::Css,
2752            "semantic:button",
2753        );
2754        let with_key_json = serde_json::to_string(&ir.nodes[0])?;
2755        assert!(with_key_json.contains("\"nodeId\":\"ir:0\""));
2756        assert!(with_key_json.contains("\"nodeKey\":\"class-selector:button#0\""));
2757        assert!(with_key_json.contains("\"nodeKeyU64\":"));
2758
2759        ir.nodes[0].clear_additive_node_key_for_test();
2760        let fallback_json = serde_json::to_string(&ir.nodes[0])?;
2761        assert!(fallback_json.contains("\"nodeId\":\"ir:0\""));
2762        assert!(!fallback_json.contains("nodeKey"));
2763        assert!(!fallback_json.contains("nodeKeyU64"));
2764        assert_eq!(ir.identity_key_at(0).as_deref(), Some("ir:0"));
2765        Ok(())
2766    }
2767
2768    #[test]
2769    fn cascade_safety_witness_evidence_graph_preserves_public_shape()
2770    -> Result<(), serde_json::Error> {
2771        let witness = cascade_safety_witness(TransformPassKind::NumberCompression);
2772
2773        let before = serde_json::to_value(witness)?;
2774        let graph = witness
2775            .evidence_graph()
2776            .map_err(|_| serde::ser::Error::custom("witness edge must target its node"))?;
2777        let after = serde_json::to_value(witness)?;
2778
2779        assert_eq!(before, after);
2780        assert_eq!(graph.nodes.len(), 1);
2781        assert_eq!(graph.nodes[0].key.input_identity, "number-compression");
2782        assert_eq!(graph.nodes[0].guarantee, GuaranteeKindV0::Floor);
2783        assert_eq!(
2784            graph.nodes[0].earned_via(),
2785            GuaranteeFamilyV0::ProseObligationDischarged
2786        );
2787        assert!(
2788            graph.nodes[0]
2789                .provenance
2790                .iter()
2791                .any(|item| item == "enforcedAt:compile-time-exhaustive-pass-catalog")
2792        );
2793        Ok(())
2794    }
2795
2796    #[test]
2797    fn transform_pass_obligation_families_preserve_catalog_obligation_text() {
2798        for kind in all_transform_pass_kinds() {
2799            assert_eq!(
2800                cascade_safe_obligation(kind),
2801                cascade_safe_obligation_reference(kind),
2802                "obligation text changed for {}",
2803                kind.id()
2804            );
2805            assert_eq!(
2806                obligation_family_for_transform_pass(kind)
2807                    .descriptor()
2808                    .obligation,
2809                cascade_safe_obligation_reference(kind),
2810                "family descriptor text changed for {}",
2811                kind.id()
2812            );
2813        }
2814    }
2815}