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