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