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