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