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