1pub use omena_parser::StyleDialect;
8use omena_parser::{
9 ParsedAnimationFactKind, ParsedCssModuleComposesFactKind, ParsedCssModuleValueFactKind,
10 ParsedIcssFactKind, ParsedSassSymbolFactKind, ParsedSelectorFactKind, ParsedVariableFactKind,
11 collect_style_facts,
12};
13use serde::Serialize;
14
15#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Serialize)]
16#[serde(rename_all = "camelCase")]
17pub enum TransformLayer {
18 SemanticReadOnly,
19 SemanticAware,
20 Commodity,
21 Emission,
22}
23
24#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Serialize)]
25#[serde(rename_all = "camelCase")]
26pub enum TransformPassKind {
27 WhitespaceStrip,
28 CommentStrip,
29 NumberCompression,
30 UnitNormalization,
31 ColorCompression,
32 UrlQuoteStrip,
33 StringQuoteNormalize,
34 SelectorIsWhereCompression,
35 ShorthandCombining,
36 RuleDeduplication,
37 RuleMerging,
38 SelectorMerging,
39 EmptyRuleRemoval,
40 VendorPrefixing,
41 LightDarkLowering,
42 ColorMixLowering,
43 OklchOklabLowering,
44 ColorFunctionLowering,
45 LogicalToPhysical,
46 NestingUnwrap,
47 ScopeFlatten,
48 LayerFlatten,
49 SupportsStaticEval,
50 MediaStaticEval,
51 CalcReduction,
52 ImportInline,
53 ScssModuleEvaluate,
54 LessModuleEvaluate,
55 HashCssModuleClassNames,
56 ResolveCssModulesComposes,
57 ValueResolution,
58 StaticVarSubstitution,
59 TreeShakeClass,
60 TreeShakeKeyframes,
61 TreeShakeValue,
62 TreeShakeCustomProperty,
63 DeadMediaBranchRemoval,
64 DeadSupportsBranchRemoval,
65 DesignTokenRouting,
66 PrintCss,
67}
68
69pub const TRANSFORM_PASS_CATALOG_LEN: usize = 40;
70
71pub const fn all_transform_pass_kinds() -> [TransformPassKind; TRANSFORM_PASS_CATALOG_LEN] {
72 [
73 TransformPassKind::WhitespaceStrip,
74 TransformPassKind::CommentStrip,
75 TransformPassKind::NumberCompression,
76 TransformPassKind::UnitNormalization,
77 TransformPassKind::ColorCompression,
78 TransformPassKind::UrlQuoteStrip,
79 TransformPassKind::StringQuoteNormalize,
80 TransformPassKind::SelectorIsWhereCompression,
81 TransformPassKind::ShorthandCombining,
82 TransformPassKind::RuleDeduplication,
83 TransformPassKind::RuleMerging,
84 TransformPassKind::SelectorMerging,
85 TransformPassKind::EmptyRuleRemoval,
86 TransformPassKind::VendorPrefixing,
87 TransformPassKind::LightDarkLowering,
88 TransformPassKind::ColorMixLowering,
89 TransformPassKind::OklchOklabLowering,
90 TransformPassKind::ColorFunctionLowering,
91 TransformPassKind::LogicalToPhysical,
92 TransformPassKind::NestingUnwrap,
93 TransformPassKind::ScopeFlatten,
94 TransformPassKind::LayerFlatten,
95 TransformPassKind::SupportsStaticEval,
96 TransformPassKind::MediaStaticEval,
97 TransformPassKind::CalcReduction,
98 TransformPassKind::ImportInline,
99 TransformPassKind::ScssModuleEvaluate,
100 TransformPassKind::LessModuleEvaluate,
101 TransformPassKind::HashCssModuleClassNames,
102 TransformPassKind::ResolveCssModulesComposes,
103 TransformPassKind::ValueResolution,
104 TransformPassKind::StaticVarSubstitution,
105 TransformPassKind::TreeShakeClass,
106 TransformPassKind::TreeShakeKeyframes,
107 TransformPassKind::TreeShakeValue,
108 TransformPassKind::TreeShakeCustomProperty,
109 TransformPassKind::DeadMediaBranchRemoval,
110 TransformPassKind::DeadSupportsBranchRemoval,
111 TransformPassKind::DesignTokenRouting,
112 TransformPassKind::PrintCss,
113 ]
114}
115
116impl TransformPassKind {
117 pub const fn ordinal(self) -> u8 {
118 match self {
119 Self::WhitespaceStrip => 1,
120 Self::CommentStrip => 2,
121 Self::NumberCompression => 3,
122 Self::UnitNormalization => 4,
123 Self::ColorCompression => 5,
124 Self::UrlQuoteStrip => 6,
125 Self::StringQuoteNormalize => 7,
126 Self::SelectorIsWhereCompression => 8,
127 Self::ShorthandCombining => 9,
128 Self::RuleDeduplication => 10,
129 Self::RuleMerging => 11,
130 Self::SelectorMerging => 12,
131 Self::EmptyRuleRemoval => 13,
132 Self::VendorPrefixing => 14,
133 Self::LightDarkLowering => 15,
134 Self::ColorMixLowering => 16,
135 Self::OklchOklabLowering => 17,
136 Self::ColorFunctionLowering => 18,
137 Self::LogicalToPhysical => 19,
138 Self::NestingUnwrap => 20,
139 Self::ScopeFlatten => 21,
140 Self::LayerFlatten => 22,
141 Self::SupportsStaticEval => 23,
142 Self::MediaStaticEval => 24,
143 Self::CalcReduction => 25,
144 Self::ImportInline => 26,
145 Self::ScssModuleEvaluate => 27,
146 Self::LessModuleEvaluate => 28,
147 Self::HashCssModuleClassNames => 29,
148 Self::ResolveCssModulesComposes => 30,
149 Self::ValueResolution => 31,
150 Self::StaticVarSubstitution => 32,
151 Self::TreeShakeClass => 33,
152 Self::TreeShakeKeyframes => 34,
153 Self::TreeShakeValue => 35,
154 Self::TreeShakeCustomProperty => 36,
155 Self::DeadMediaBranchRemoval => 37,
156 Self::DeadSupportsBranchRemoval => 38,
157 Self::DesignTokenRouting => 39,
158 Self::PrintCss => 40,
159 }
160 }
161
162 pub const fn label(self) -> &'static str {
163 self.id()
164 }
165
166 pub const fn title(self) -> &'static str {
167 match self {
168 Self::WhitespaceStrip => "whitespace strip",
169 Self::CommentStrip => "comment strip",
170 Self::NumberCompression => "number compression",
171 Self::UnitNormalization => "unit normalization",
172 Self::ColorCompression => "color compression",
173 Self::UrlQuoteStrip => "url quote strip",
174 Self::StringQuoteNormalize => "string and font value normalize",
175 Self::SelectorIsWhereCompression => "selector alias compression",
176 Self::ShorthandCombining => "shorthand combining",
177 Self::RuleDeduplication => "rule deduplication",
178 Self::RuleMerging => "rule merging",
179 Self::SelectorMerging => "selector merging",
180 Self::EmptyRuleRemoval => "empty rule removal",
181 Self::VendorPrefixing => "vendor prefixing",
182 Self::LightDarkLowering => "light-dark lowering",
183 Self::ColorMixLowering => "color-mix lowering",
184 Self::OklchOklabLowering => "oklch/oklab lowering",
185 Self::ColorFunctionLowering => "color() lowering",
186 Self::LogicalToPhysical => "logical to physical",
187 Self::NestingUnwrap => "nesting unwrap",
188 Self::ScopeFlatten => "@scope flatten",
189 Self::LayerFlatten => "@layer flatten",
190 Self::SupportsStaticEval => "@supports static eval",
191 Self::MediaStaticEval => "@media static eval",
192 Self::CalcReduction => "calc() reduction",
193 Self::ImportInline => "@import inline",
194 Self::ScssModuleEvaluate => "SCSS module evaluate",
195 Self::LessModuleEvaluate => "Less module evaluate",
196 Self::HashCssModuleClassNames => "CSS Modules class hashing",
197 Self::ResolveCssModulesComposes => "composes resolution",
198 Self::ValueResolution => "@value resolution",
199 Self::StaticVarSubstitution => "custom property static resolve",
200 Self::TreeShakeClass => "tree shaking class",
201 Self::TreeShakeKeyframes => "tree shaking keyframes",
202 Self::TreeShakeValue => "tree shaking value",
203 Self::TreeShakeCustomProperty => "tree shaking custom-property",
204 Self::DeadMediaBranchRemoval => "dead @media branch removal",
205 Self::DeadSupportsBranchRemoval => "dead @supports branch removal",
206 Self::DesignTokenRouting => "design-token routing",
207 Self::PrintCss => "printer + sourcemap composer",
208 }
209 }
210
211 pub const fn id(self) -> &'static str {
212 match self {
213 Self::WhitespaceStrip => "whitespace-strip",
214 Self::CommentStrip => "comment-strip",
215 Self::NumberCompression => "number-compression",
216 Self::UnitNormalization => "unit-normalization",
217 Self::ColorCompression => "color-compression",
218 Self::UrlQuoteStrip => "url-quote-strip",
219 Self::StringQuoteNormalize => "string-quote-normalize",
220 Self::SelectorIsWhereCompression => "selector-is-where-compression",
221 Self::ShorthandCombining => "shorthand-combining",
222 Self::RuleDeduplication => "rule-deduplication",
223 Self::RuleMerging => "rule-merging",
224 Self::SelectorMerging => "selector-merging",
225 Self::EmptyRuleRemoval => "empty-rule-removal",
226 Self::VendorPrefixing => "vendor-prefixing",
227 Self::LightDarkLowering => "light-dark-lowering",
228 Self::ColorMixLowering => "color-mix-lowering",
229 Self::OklchOklabLowering => "oklch-oklab-lowering",
230 Self::ColorFunctionLowering => "color-function-lowering",
231 Self::LogicalToPhysical => "logical-to-physical",
232 Self::NestingUnwrap => "nesting-unwrap",
233 Self::ScopeFlatten => "scope-flatten",
234 Self::LayerFlatten => "layer-flatten",
235 Self::SupportsStaticEval => "supports-static-eval",
236 Self::MediaStaticEval => "media-static-eval",
237 Self::CalcReduction => "calc-reduction",
238 Self::ImportInline => "import-inline",
239 Self::ScssModuleEvaluate => "scss-module-evaluate",
240 Self::LessModuleEvaluate => "less-module-evaluate",
241 Self::HashCssModuleClassNames => "css-modules-class-hashing",
242 Self::ResolveCssModulesComposes => "composes-resolution",
243 Self::ValueResolution => "value-resolution",
244 Self::StaticVarSubstitution => "custom-property-static-resolve",
245 Self::TreeShakeClass => "tree-shake-class",
246 Self::TreeShakeKeyframes => "tree-shake-keyframes",
247 Self::TreeShakeValue => "tree-shake-value",
248 Self::TreeShakeCustomProperty => "tree-shake-custom-property",
249 Self::DeadMediaBranchRemoval => "dead-media-branch-removal",
250 Self::DeadSupportsBranchRemoval => "dead-supports-branch-removal",
251 Self::DesignTokenRouting => "design-token-routing",
252 Self::PrintCss => "print-css",
253 }
254 }
255
256 pub const fn layer(self) -> TransformLayer {
257 match self {
258 Self::ImportInline
259 | Self::ScssModuleEvaluate
260 | Self::LessModuleEvaluate
261 | Self::HashCssModuleClassNames
262 | Self::ResolveCssModulesComposes
263 | Self::ValueResolution
264 | Self::StaticVarSubstitution
265 | Self::TreeShakeClass
266 | Self::TreeShakeKeyframes
267 | Self::TreeShakeValue
268 | Self::TreeShakeCustomProperty
269 | Self::DeadMediaBranchRemoval
270 | Self::DeadSupportsBranchRemoval
271 | Self::DesignTokenRouting => TransformLayer::SemanticAware,
272 Self::PrintCss => TransformLayer::Emission,
273 _ => TransformLayer::Commodity,
274 }
275 }
276
277 pub const fn reads_semantic_graph(self) -> bool {
278 matches!(
279 self,
280 Self::ImportInline
281 | Self::ScssModuleEvaluate
282 | Self::LessModuleEvaluate
283 | Self::HashCssModuleClassNames
284 | Self::ResolveCssModulesComposes
285 | Self::ValueResolution
286 | Self::StaticVarSubstitution
287 | Self::TreeShakeClass
288 | Self::TreeShakeKeyframes
289 | Self::TreeShakeValue
290 | Self::TreeShakeCustomProperty
291 | Self::DeadMediaBranchRemoval
292 | Self::DeadSupportsBranchRemoval
293 | Self::DesignTokenRouting
294 )
295 }
296
297 pub const fn reads_cascade_model(self) -> bool {
298 matches!(
299 self,
300 Self::ShorthandCombining
301 | Self::RuleDeduplication
302 | Self::RuleMerging
303 | Self::SelectorMerging
304 | Self::ScopeFlatten
305 | Self::LayerFlatten
306 | Self::StaticVarSubstitution
307 | Self::DeadMediaBranchRemoval
308 | Self::DeadSupportsBranchRemoval
309 )
310 }
311
312 pub const fn read_model(self) -> TransformPassReadModel {
313 match self {
314 Self::VendorPrefixing
315 | Self::LightDarkLowering
316 | Self::ColorMixLowering
317 | Self::OklchOklabLowering
318 | Self::ColorFunctionLowering
319 | Self::LogicalToPhysical
320 | Self::NestingUnwrap => TransformPassReadModel::TargetData,
321 Self::ShorthandCombining
322 | Self::RuleDeduplication
323 | Self::RuleMerging
324 | Self::SelectorMerging
325 | Self::ScopeFlatten
326 | Self::LayerFlatten
327 | Self::StaticVarSubstitution
328 | Self::DeadMediaBranchRemoval
329 | Self::DeadSupportsBranchRemoval => TransformPassReadModel::CascadeModel,
330 Self::TreeShakeClass
331 | Self::TreeShakeKeyframes
332 | Self::TreeShakeValue
333 | Self::TreeShakeCustomProperty
334 | Self::DesignTokenRouting => TransformPassReadModel::BridgeReachability,
335 Self::ImportInline
336 | Self::ScssModuleEvaluate
337 | Self::LessModuleEvaluate
338 | Self::HashCssModuleClassNames
339 | Self::ResolveCssModulesComposes
340 | Self::ValueResolution => TransformPassReadModel::SemanticGraph,
341 Self::PrintCss => TransformPassReadModel::Emission,
342 _ => TransformPassReadModel::SyntaxOnly,
343 }
344 }
345}
346
347#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize)]
348#[serde(rename_all = "camelCase")]
349pub enum TransformPassReadModel {
350 SyntaxOnly,
351 TargetData,
352 CascadeModel,
353 SemanticGraph,
354 BridgeReachability,
355 Emission,
356}
357
358#[derive(Debug, Clone, PartialEq, Eq, Serialize)]
359#[serde(rename_all = "camelCase")]
360pub struct TransformPassContractV0 {
361 pub ordinal: u8,
362 pub label: &'static str,
363 pub id: &'static str,
364 pub title: &'static str,
365 pub kind: TransformPassKind,
366 pub layer: TransformLayer,
367 pub read_model: TransformPassReadModel,
368 pub reads_semantic_graph: bool,
369 pub reads_cascade_model: bool,
370 pub writes_css: bool,
371 pub cascade_safe: bool,
372 pub cascade_safety_witness: CascadeSafetyWitnessV0,
373 pub cascade_safe_obligation: &'static str,
374}
375
376#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize)]
377#[serde(rename_all = "camelCase")]
378pub struct CascadeSafetyWitnessV0 {
379 pub pass_id: &'static str,
380 pub obligation: &'static str,
381 pub enforced_at: &'static str,
382}
383
384#[derive(Debug, Clone, PartialEq, Eq, Serialize)]
385#[serde(rename_all = "camelCase")]
386pub struct TransformDagEdgeV0 {
387 pub from: &'static str,
388 pub to: &'static str,
389 pub reason: &'static str,
390}
391
392#[derive(Debug, Clone, PartialEq, Eq, Serialize)]
393#[serde(rename_all = "camelCase")]
394pub struct TransformCstBoundarySummaryV0 {
395 pub schema_version: &'static str,
396 pub product: &'static str,
397 pub representation: &'static str,
398 pub pass_contracts: Vec<TransformPassContractV0>,
399 pub dag_edges: Vec<TransformDagEdgeV0>,
400 pub pass_catalog_count: usize,
401 pub semantic_aware_pass_count: usize,
402 pub commodity_pass_count: usize,
403 pub emission_pass_count: usize,
404 pub full_pass_catalog_covered: bool,
405 pub all_passes_declare_cascade_obligation: bool,
406 pub all_passes_have_compile_time_cascade_witness: bool,
407 pub stable_transform_ir_ready: bool,
408 pub provenance_derivation_forest_scaffold_ready: bool,
409 pub provenance_preservation_required: bool,
410 pub next_surfaces: Vec<&'static str>,
411}
412
413#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Serialize)]
414#[serde(rename_all = "camelCase")]
415pub enum StableTransformIrNodeKindV0 {
416 ClassSelector,
417 IdSelector,
418 PlaceholderSelector,
419 CustomPropertyDeclaration,
420 CustomPropertyReference,
421 ScssVariableDeclaration,
422 ScssVariableReference,
423 LessVariableDeclaration,
424 LessVariableReference,
425 SassSymbolDeclaration,
426 SassSymbolReference,
427 SassModuleEdge,
428 KeyframesDeclaration,
429 AnimationNameReference,
430 CssModuleValueDefinition,
431 CssModuleValueReference,
432 CssModuleValueImportSource,
433 CssModuleComposesTarget,
434 CssModuleComposesImportSource,
435 IcssExportName,
436 IcssImportLocalName,
437 IcssImportRemoteName,
438 IcssImportSource,
439 AtRule,
440}
441
442impl StableTransformIrNodeKindV0 {
443 pub const fn id(self) -> &'static str {
444 match self {
445 Self::ClassSelector => "class-selector",
446 Self::IdSelector => "id-selector",
447 Self::PlaceholderSelector => "placeholder-selector",
448 Self::CustomPropertyDeclaration => "custom-property-declaration",
449 Self::CustomPropertyReference => "custom-property-reference",
450 Self::ScssVariableDeclaration => "scss-variable-declaration",
451 Self::ScssVariableReference => "scss-variable-reference",
452 Self::LessVariableDeclaration => "less-variable-declaration",
453 Self::LessVariableReference => "less-variable-reference",
454 Self::SassSymbolDeclaration => "sass-symbol-declaration",
455 Self::SassSymbolReference => "sass-symbol-reference",
456 Self::SassModuleEdge => "sass-module-edge",
457 Self::KeyframesDeclaration => "keyframes-declaration",
458 Self::AnimationNameReference => "animation-name-reference",
459 Self::CssModuleValueDefinition => "css-module-value-definition",
460 Self::CssModuleValueReference => "css-module-value-reference",
461 Self::CssModuleValueImportSource => "css-module-value-import-source",
462 Self::CssModuleComposesTarget => "css-module-composes-target",
463 Self::CssModuleComposesImportSource => "css-module-composes-import-source",
464 Self::IcssExportName => "icss-export-name",
465 Self::IcssImportLocalName => "icss-import-local-name",
466 Self::IcssImportRemoteName => "icss-import-remote-name",
467 Self::IcssImportSource => "icss-import-source",
468 Self::AtRule => "at-rule",
469 }
470 }
471}
472
473#[derive(Debug, Clone, PartialEq, Eq, Serialize)]
474#[serde(rename_all = "camelCase")]
475pub struct StableTransformIrNodeV0 {
476 pub node_id: String,
477 pub kind: StableTransformIrNodeKindV0,
478 pub kind_id: &'static str,
479 pub label: String,
480 pub semantic_key: String,
481 pub source_span_start: usize,
482 pub source_span_end: usize,
483 pub provenance_anchor_index: usize,
484}
485
486#[derive(Debug, Clone, PartialEq, Eq, Serialize)]
487#[serde(rename_all = "camelCase")]
488pub struct TransformCstProvenanceAnchorV0 {
489 pub anchor_index: usize,
490 pub node_id: String,
491 pub semantic_key: String,
492 pub source_span_start: usize,
493 pub source_span_end: usize,
494}
495
496#[derive(Debug, Clone, PartialEq, Eq, Serialize)]
497#[serde(rename_all = "camelCase")]
498pub struct StableTransformIrV0 {
499 pub schema_version: &'static str,
500 pub product: &'static str,
501 pub dialect: &'static str,
502 pub source_byte_len: usize,
503 pub semantic_signature: String,
504 pub node_count: usize,
505 pub parser_error_count: usize,
506 pub contains_bogus_or_trivia: bool,
507 pub stable_post_semantic_ir: bool,
508 pub nodes: Vec<StableTransformIrNodeV0>,
509 pub provenance_anchors: Vec<TransformCstProvenanceAnchorV0>,
510}
511
512#[derive(Debug, Clone, PartialEq, Eq, Serialize)]
513#[serde(rename_all = "camelCase")]
514pub struct TransformCstArtifactV0 {
515 pub schema_version: &'static str,
516 pub product: &'static str,
517 pub source_byte_len: usize,
518 pub semantic_signature: String,
519 pub stable_ir: StableTransformIrV0,
520 pub stable_ir_node_count: usize,
521 pub parser_error_count: usize,
522 pub contains_bogus_or_trivia: bool,
523 pub pass_ids: Vec<&'static str>,
524 pub provenance_preserved: bool,
525}
526
527pub fn summarize_omena_transform_cst_boundary() -> TransformCstBoundarySummaryV0 {
528 let pass_contracts = default_transform_pass_contracts();
529 let semantic_aware_pass_count = pass_contracts
530 .iter()
531 .filter(|contract| contract.layer == TransformLayer::SemanticAware)
532 .count();
533 let commodity_pass_count = pass_contracts
534 .iter()
535 .filter(|contract| contract.layer == TransformLayer::Commodity)
536 .count();
537 let emission_pass_count = pass_contracts
538 .iter()
539 .filter(|contract| contract.layer == TransformLayer::Emission)
540 .count();
541 let all_passes_declare_cascade_obligation = pass_contracts
542 .iter()
543 .all(|contract| !contract.cascade_safe_obligation.is_empty());
544 let all_passes_have_compile_time_cascade_witness = pass_contracts.iter().all(|contract| {
545 contract.cascade_safe
546 && contract.cascade_safety_witness.pass_id == contract.id
547 && contract.cascade_safety_witness.obligation == contract.cascade_safe_obligation
548 && contract.cascade_safety_witness.enforced_at == "compile-time-exhaustive-pass-catalog"
549 });
550 let pass_catalog_count = pass_contracts.len();
551
552 TransformCstBoundarySummaryV0 {
553 schema_version: "0",
554 product: "omena-transform-cst.boundary",
555 representation: "post-semantic-provenance-preserving-transform-cst",
556 pass_contracts,
557 dag_edges: default_transform_dag_edges(),
558 pass_catalog_count,
559 semantic_aware_pass_count,
560 commodity_pass_count,
561 emission_pass_count,
562 full_pass_catalog_covered: pass_catalog_count == TRANSFORM_PASS_CATALOG_LEN,
563 all_passes_declare_cascade_obligation,
564 all_passes_have_compile_time_cascade_witness,
565 stable_transform_ir_ready: true,
566 provenance_derivation_forest_scaffold_ready: true,
567 provenance_preservation_required: true,
568 next_surfaces: Vec::new(),
569 }
570}
571
572pub fn build_transform_cst_artifact(
573 source: &str,
574 semantic_signature: impl Into<String>,
575 passes: &[TransformPassKind],
576) -> TransformCstArtifactV0 {
577 build_transform_cst_artifact_with_dialect(source, StyleDialect::Css, semantic_signature, passes)
578}
579
580pub fn build_transform_cst_artifact_with_dialect(
581 source: &str,
582 dialect: StyleDialect,
583 semantic_signature: impl Into<String>,
584 passes: &[TransformPassKind],
585) -> TransformCstArtifactV0 {
586 let semantic_signature = semantic_signature.into();
587 let stable_ir =
588 build_stable_transform_ir_from_source(source, dialect, semantic_signature.clone());
589 let stable_ir_node_count = stable_ir.node_count;
590 let parser_error_count = stable_ir.parser_error_count;
591 let contains_bogus_or_trivia = stable_ir.contains_bogus_or_trivia;
592
593 TransformCstArtifactV0 {
594 schema_version: "0",
595 product: "omena-transform-cst.artifact",
596 source_byte_len: source.len(),
597 semantic_signature,
598 stable_ir,
599 stable_ir_node_count,
600 parser_error_count,
601 contains_bogus_or_trivia,
602 pass_ids: passes.iter().map(|pass| pass.id()).collect(),
603 provenance_preserved: true,
604 }
605}
606
607pub fn build_stable_transform_ir_from_source(
608 source: &str,
609 dialect: StyleDialect,
610 semantic_signature: impl Into<String>,
611) -> StableTransformIrV0 {
612 let facts = collect_style_facts(source, dialect);
613 let mut nodes = Vec::new();
614
615 for selector in facts.selectors {
616 push_ir_node(
617 &mut nodes,
618 stable_ir_selector_kind(selector.kind),
619 selector.name,
620 selector.range.start().into(),
621 selector.range.end().into(),
622 );
623 }
624
625 for variable in facts.variables {
626 push_ir_node(
627 &mut nodes,
628 stable_ir_variable_kind(variable.kind),
629 variable.name,
630 variable.range.start().into(),
631 variable.range.end().into(),
632 );
633 }
634
635 for symbol in facts.sass_symbols {
636 push_ir_node(
637 &mut nodes,
638 stable_ir_sass_symbol_kind(symbol.kind),
639 format!("{}:{}", symbol.symbol_kind, symbol.name),
640 symbol.range.start().into(),
641 symbol.range.end().into(),
642 );
643 }
644
645 for edge in facts.sass_module_edges {
646 push_ir_node(
647 &mut nodes,
648 StableTransformIrNodeKindV0::SassModuleEdge,
649 edge.source,
650 edge.range.start().into(),
651 edge.range.end().into(),
652 );
653 }
654
655 for animation in facts.animations {
656 push_ir_node(
657 &mut nodes,
658 stable_ir_animation_kind(animation.kind),
659 animation.name,
660 animation.range.start().into(),
661 animation.range.end().into(),
662 );
663 }
664
665 for value in facts.css_module_values {
666 push_ir_node(
667 &mut nodes,
668 stable_ir_css_module_value_kind(value.kind),
669 value.name,
670 value.range.start().into(),
671 value.range.end().into(),
672 );
673 }
674
675 for composes in facts.css_module_composes {
676 push_ir_node(
677 &mut nodes,
678 stable_ir_css_module_composes_kind(composes.kind),
679 composes.name,
680 composes.range.start().into(),
681 composes.range.end().into(),
682 );
683 }
684
685 for icss in facts.icss {
686 push_ir_node(
687 &mut nodes,
688 stable_ir_icss_kind(icss.kind),
689 icss.name,
690 icss.range.start().into(),
691 icss.range.end().into(),
692 );
693 }
694
695 for at_rule in facts.at_rules {
696 push_ir_node(
697 &mut nodes,
698 StableTransformIrNodeKindV0::AtRule,
699 at_rule.name,
700 at_rule.range.start().into(),
701 at_rule.range.end().into(),
702 );
703 }
704
705 nodes.sort_by(|left, right| {
706 left.source_span_start
707 .cmp(&right.source_span_start)
708 .then_with(|| left.source_span_end.cmp(&right.source_span_end))
709 .then_with(|| left.kind.cmp(&right.kind))
710 .then_with(|| left.label.cmp(&right.label))
711 });
712
713 let mut provenance_anchors = Vec::with_capacity(nodes.len());
714 for (index, node) in nodes.iter_mut().enumerate() {
715 node.node_id = format!("ir:{index}");
716 node.provenance_anchor_index = index;
717 provenance_anchors.push(TransformCstProvenanceAnchorV0 {
718 anchor_index: index,
719 node_id: node.node_id.clone(),
720 semantic_key: node.semantic_key.clone(),
721 source_span_start: node.source_span_start,
722 source_span_end: node.source_span_end,
723 });
724 }
725
726 let node_count = nodes.len();
727 let parser_error_count = facts.error_count;
728
729 StableTransformIrV0 {
730 schema_version: "0",
731 product: "omena-transform-cst.stable-ir",
732 dialect: transform_cst_style_dialect_label(dialect),
733 source_byte_len: source.len(),
734 semantic_signature: semantic_signature.into(),
735 node_count,
736 parser_error_count,
737 contains_bogus_or_trivia: false,
738 stable_post_semantic_ir: parser_error_count == 0,
739 nodes,
740 provenance_anchors,
741 }
742}
743
744pub fn default_transform_pass_contracts() -> Vec<TransformPassContractV0> {
745 all_transform_pass_kinds()
746 .into_iter()
747 .map(transform_pass_contract)
748 .collect()
749}
750
751fn transform_pass_contract(kind: TransformPassKind) -> TransformPassContractV0 {
752 let cascade_safety_witness = cascade_safety_witness(kind);
753
754 TransformPassContractV0 {
755 ordinal: kind.ordinal(),
756 label: kind.label(),
757 id: kind.id(),
758 title: kind.title(),
759 kind,
760 layer: kind.layer(),
761 read_model: kind.read_model(),
762 reads_semantic_graph: kind.reads_semantic_graph(),
763 reads_cascade_model: kind.reads_cascade_model(),
764 writes_css: true,
765 cascade_safe: true,
766 cascade_safety_witness,
767 cascade_safe_obligation: cascade_safety_witness.obligation,
768 }
769}
770
771pub const fn cascade_safety_witness(kind: TransformPassKind) -> CascadeSafetyWitnessV0 {
772 CascadeSafetyWitnessV0 {
773 pass_id: kind.id(),
774 obligation: cascade_safe_obligation(kind),
775 enforced_at: "compile-time-exhaustive-pass-catalog",
776 }
777}
778
779pub const fn cascade_safe_obligation(kind: TransformPassKind) -> &'static str {
780 match kind {
781 TransformPassKind::WhitespaceStrip => {
782 "may remove only whitespace outside string, url, attr, and calc-sensitive token boundaries"
783 }
784 TransformPassKind::CommentStrip => {
785 "may remove comments only when source-map provenance preserves the removed span"
786 }
787 TransformPassKind::NumberCompression => {
788 "may rewrite only numerically equivalent literal tokens"
789 }
790 TransformPassKind::UnitNormalization => {
791 "may normalize only dimension values whose computed value is unchanged"
792 }
793 TransformPassKind::ColorCompression => "may rewrite only color-equivalent literal tokens",
794 TransformPassKind::UrlQuoteStrip => {
795 "may remove url quotes only when the unquoted token grammar remains equivalent"
796 }
797 TransformPassKind::StringQuoteNormalize => {
798 "may normalize string quotes and font keyword aliases only when computed text and font values remain equivalent"
799 }
800 TransformPassKind::SelectorIsWhereCompression => {
801 "must preserve selector specificity, keyframe timeline positions, and matching semantics under the cascade model"
802 }
803 TransformPassKind::ShorthandCombining => {
804 "must prove longhand and shorthand cascade outcomes are equivalent"
805 }
806 TransformPassKind::RuleDeduplication => {
807 "must preserve origin, layer, specificity, and order for every surviving declaration"
808 }
809 TransformPassKind::RuleMerging => {
810 "must prove merged rule order cannot change declaration winners"
811 }
812 TransformPassKind::SelectorMerging => {
813 "must preserve selector identity and post-hash module semantics"
814 }
815 TransformPassKind::EmptyRuleRemoval => {
816 "may remove rules only when no source-visible semantic marker is attached"
817 }
818 TransformPassKind::VendorPrefixing => {
819 "must add target-required prefixed declarations without changing modern target outcomes"
820 }
821 TransformPassKind::LightDarkLowering => {
822 "must lower only when target data requires fallback branches and provenance tracks both branches"
823 }
824 TransformPassKind::ColorMixLowering => {
825 "must lower only when color-space conversion is target-equivalent"
826 }
827 TransformPassKind::OklchOklabLowering => {
828 "must preserve color semantics within the configured target fallback precision"
829 }
830 TransformPassKind::ColorFunctionLowering => {
831 "must preserve color semantics within the configured target fallback precision"
832 }
833 TransformPassKind::LogicalToPhysical => {
834 "must run only under explicit directionality options"
835 }
836 TransformPassKind::NestingUnwrap => {
837 "must preserve nested selector expansion and specificity"
838 }
839 TransformPassKind::ScopeFlatten => {
840 "must preserve scoped matching semantics or emit a blocked result"
841 }
842 TransformPassKind::LayerFlatten => "must preserve layer order in CascadeKey comparison",
843 TransformPassKind::SupportsStaticEval => {
844 "may remove branches only when the target feature predicate is known"
845 }
846 TransformPassKind::MediaStaticEval => {
847 "may remove branches only when the configured media predicate is known"
848 }
849 TransformPassKind::CalcReduction => {
850 "may reduce only syntax-equivalent or computed-value-equivalent calc expressions"
851 }
852 TransformPassKind::ImportInline => {
853 "must preserve import-site media, supports, layer wrappers, and source provenance"
854 }
855 TransformPassKind::ScssModuleEvaluate => {
856 "must preserve SCSS namespace, show/hide, mixin, variable, and source provenance facts"
857 }
858 TransformPassKind::LessModuleEvaluate => {
859 "must preserve Less variable, mixin, namespace, and source provenance facts"
860 }
861 TransformPassKind::HashCssModuleClassNames => {
862 "must rewrite every source and style reference through the same selector identity map"
863 }
864 TransformPassKind::ResolveCssModulesComposes => {
865 "must preserve exported class set and composed class provenance"
866 }
867 TransformPassKind::ValueResolution => {
868 "must preserve @value graph resolution and cycle diagnostics"
869 }
870 TransformPassKind::StaticVarSubstitution => {
871 "must preserve custom-property fixed-point semantics or emit a provenance-backed blocked result"
872 }
873 TransformPassKind::TreeShakeClass => {
874 "may remove classes only when bridge reachability proves no reachable source expression observes them"
875 }
876 TransformPassKind::TreeShakeKeyframes => {
877 "may remove keyframes only when animation-name reachability proves they are unobservable"
878 }
879 TransformPassKind::TreeShakeValue => {
880 "may remove @value declarations only when value-graph traversal proves they are unreachable"
881 }
882 TransformPassKind::TreeShakeCustomProperty => {
883 "may remove custom properties only when var() reachability proves they are unobservable"
884 }
885 TransformPassKind::DeadMediaBranchRemoval => {
886 "may remove @media branches only when target and cascade witnesses prove deadness"
887 }
888 TransformPassKind::DeadSupportsBranchRemoval => {
889 "may remove @supports branches only when target and cascade witnesses prove deadness"
890 }
891 TransformPassKind::DesignTokenRouting => {
892 "must preserve design-token provenance while routing declarations across package boundaries"
893 }
894 TransformPassKind::PrintCss => {
895 "must emit a source-map trace for every non-trivia transformed span"
896 }
897 }
898}
899
900fn push_ir_node(
901 nodes: &mut Vec<StableTransformIrNodeV0>,
902 kind: StableTransformIrNodeKindV0,
903 label: impl Into<String>,
904 source_span_start: usize,
905 source_span_end: usize,
906) {
907 let label = label.into();
908 let kind_id = kind.id();
909 nodes.push(StableTransformIrNodeV0 {
910 node_id: String::new(),
911 kind,
912 kind_id,
913 semantic_key: format!("{kind_id}:{label}"),
914 label,
915 source_span_start,
916 source_span_end,
917 provenance_anchor_index: 0,
918 });
919}
920
921const fn stable_ir_selector_kind(kind: ParsedSelectorFactKind) -> StableTransformIrNodeKindV0 {
922 match kind {
923 ParsedSelectorFactKind::Class => StableTransformIrNodeKindV0::ClassSelector,
924 ParsedSelectorFactKind::Id => StableTransformIrNodeKindV0::IdSelector,
925 ParsedSelectorFactKind::Placeholder => StableTransformIrNodeKindV0::PlaceholderSelector,
926 }
927}
928
929const fn stable_ir_variable_kind(kind: ParsedVariableFactKind) -> StableTransformIrNodeKindV0 {
930 match kind {
931 ParsedVariableFactKind::ScssDeclaration => {
932 StableTransformIrNodeKindV0::ScssVariableDeclaration
933 }
934 ParsedVariableFactKind::ScssReference => StableTransformIrNodeKindV0::ScssVariableReference,
935 ParsedVariableFactKind::LessDeclaration => {
936 StableTransformIrNodeKindV0::LessVariableDeclaration
937 }
938 ParsedVariableFactKind::LessReference => StableTransformIrNodeKindV0::LessVariableReference,
939 ParsedVariableFactKind::CustomPropertyDeclaration => {
940 StableTransformIrNodeKindV0::CustomPropertyDeclaration
941 }
942 ParsedVariableFactKind::CustomPropertyReference => {
943 StableTransformIrNodeKindV0::CustomPropertyReference
944 }
945 }
946}
947
948const fn stable_ir_sass_symbol_kind(kind: ParsedSassSymbolFactKind) -> StableTransformIrNodeKindV0 {
949 match kind {
950 ParsedSassSymbolFactKind::VariableDeclaration
951 | ParsedSassSymbolFactKind::MixinDeclaration
952 | ParsedSassSymbolFactKind::FunctionDeclaration => {
953 StableTransformIrNodeKindV0::SassSymbolDeclaration
954 }
955 ParsedSassSymbolFactKind::VariableReference
956 | ParsedSassSymbolFactKind::MixinInclude
957 | ParsedSassSymbolFactKind::FunctionCall => {
958 StableTransformIrNodeKindV0::SassSymbolReference
959 }
960 }
961}
962
963const fn stable_ir_animation_kind(kind: ParsedAnimationFactKind) -> StableTransformIrNodeKindV0 {
964 match kind {
965 ParsedAnimationFactKind::KeyframesDeclaration => {
966 StableTransformIrNodeKindV0::KeyframesDeclaration
967 }
968 ParsedAnimationFactKind::AnimationNameReference => {
969 StableTransformIrNodeKindV0::AnimationNameReference
970 }
971 }
972}
973
974const fn stable_ir_css_module_value_kind(
975 kind: ParsedCssModuleValueFactKind,
976) -> StableTransformIrNodeKindV0 {
977 match kind {
978 ParsedCssModuleValueFactKind::Definition => {
979 StableTransformIrNodeKindV0::CssModuleValueDefinition
980 }
981 ParsedCssModuleValueFactKind::Reference => {
982 StableTransformIrNodeKindV0::CssModuleValueReference
983 }
984 ParsedCssModuleValueFactKind::ImportSource => {
985 StableTransformIrNodeKindV0::CssModuleValueImportSource
986 }
987 }
988}
989
990const fn stable_ir_css_module_composes_kind(
991 kind: ParsedCssModuleComposesFactKind,
992) -> StableTransformIrNodeKindV0 {
993 match kind {
994 ParsedCssModuleComposesFactKind::Target => {
995 StableTransformIrNodeKindV0::CssModuleComposesTarget
996 }
997 ParsedCssModuleComposesFactKind::ImportSource => {
998 StableTransformIrNodeKindV0::CssModuleComposesImportSource
999 }
1000 }
1001}
1002
1003const fn stable_ir_icss_kind(kind: ParsedIcssFactKind) -> StableTransformIrNodeKindV0 {
1004 match kind {
1005 ParsedIcssFactKind::ExportName => StableTransformIrNodeKindV0::IcssExportName,
1006 ParsedIcssFactKind::ImportLocalName => StableTransformIrNodeKindV0::IcssImportLocalName,
1007 ParsedIcssFactKind::ImportRemoteName => StableTransformIrNodeKindV0::IcssImportRemoteName,
1008 ParsedIcssFactKind::ImportSource => StableTransformIrNodeKindV0::IcssImportSource,
1009 }
1010}
1011
1012pub const fn transform_cst_style_dialect_label(dialect: StyleDialect) -> &'static str {
1013 match dialect {
1014 StyleDialect::Css => "css",
1015 StyleDialect::Scss => "scss",
1016 StyleDialect::Sass => "sass",
1017 StyleDialect::Less => "less",
1018 }
1019}
1020
1021pub fn default_transform_dag_edges() -> Vec<TransformDagEdgeV0> {
1022 vec![
1023 TransformDagEdgeV0 {
1024 from: "import-inline",
1025 to: "custom-property-static-resolve",
1026 reason: "var() resolution needs the full custom-property graph from inlined files",
1027 },
1028 TransformDagEdgeV0 {
1029 from: "scss-module-evaluate",
1030 to: "custom-property-static-resolve",
1031 reason: "SCSS evaluation can introduce custom-property declarations",
1032 },
1033 TransformDagEdgeV0 {
1034 from: "less-module-evaluate",
1035 to: "custom-property-static-resolve",
1036 reason: "Less evaluation can introduce custom-property declarations",
1037 },
1038 TransformDagEdgeV0 {
1039 from: "composes-resolution",
1040 to: "css-modules-class-hashing",
1041 reason: "hashing must run after composed class expansion",
1042 },
1043 TransformDagEdgeV0 {
1044 from: "nesting-unwrap",
1045 to: "css-modules-class-hashing",
1046 reason: "hashing must run after nested selectors are expanded into final selector branches",
1047 },
1048 TransformDagEdgeV0 {
1049 from: "css-modules-class-hashing",
1050 to: "selector-merging",
1051 reason: "selector merging must see post-hash selector identities",
1052 },
1053 TransformDagEdgeV0 {
1054 from: "number-compression",
1055 to: "selector-merging",
1056 reason: "selector merging must see canonical declaration numeric values",
1057 },
1058 TransformDagEdgeV0 {
1059 from: "unit-normalization",
1060 to: "selector-merging",
1061 reason: "selector merging must see canonical declaration unit values",
1062 },
1063 TransformDagEdgeV0 {
1064 from: "color-compression",
1065 to: "selector-merging",
1066 reason: "selector merging must see canonical declaration color values",
1067 },
1068 TransformDagEdgeV0 {
1069 from: "url-quote-strip",
1070 to: "selector-merging",
1071 reason: "selector merging must see canonical url() values",
1072 },
1073 TransformDagEdgeV0 {
1074 from: "string-quote-normalize",
1075 to: "selector-merging",
1076 reason: "selector merging must see canonical string values",
1077 },
1078 TransformDagEdgeV0 {
1079 from: "shorthand-combining",
1080 to: "selector-merging",
1081 reason: "selector merging must see canonical shorthand declaration blocks",
1082 },
1083 TransformDagEdgeV0 {
1084 from: "calc-reduction",
1085 to: "selector-merging",
1086 reason: "selector merging must see reduced calc() declaration values",
1087 },
1088 TransformDagEdgeV0 {
1089 from: "selector-merging",
1090 to: "whitespace-strip",
1091 reason: "whitespace stripping must run after selector merging emits final selector lists",
1092 },
1093 TransformDagEdgeV0 {
1094 from: "custom-property-static-resolve",
1095 to: "calc-reduction",
1096 reason: "var() inside calc may resolve to numeric literals that enable reduction",
1097 },
1098 TransformDagEdgeV0 {
1099 from: "value-resolution",
1100 to: "supports-static-eval",
1101 reason: "@value references inside @supports preludes must resolve before static branch evaluation",
1102 },
1103 TransformDagEdgeV0 {
1104 from: "value-resolution",
1105 to: "media-static-eval",
1106 reason: "@value references inside @media preludes must resolve before static media normalization",
1107 },
1108 TransformDagEdgeV0 {
1109 from: "custom-property-static-resolve",
1110 to: "supports-static-eval",
1111 reason: "var() references inside @supports preludes must resolve before static branch evaluation",
1112 },
1113 TransformDagEdgeV0 {
1114 from: "custom-property-static-resolve",
1115 to: "media-static-eval",
1116 reason: "var() references inside @media preludes must resolve before static media normalization",
1117 },
1118 TransformDagEdgeV0 {
1119 from: "tree-shake-class",
1120 to: "rule-deduplication",
1121 reason: "tree shaking must run before rule deduplication can hide dead rules",
1122 },
1123 TransformDagEdgeV0 {
1124 from: "tree-shake-keyframes",
1125 to: "rule-deduplication",
1126 reason: "keyframe reachability must settle before rule deduplication",
1127 },
1128 TransformDagEdgeV0 {
1129 from: "tree-shake-value",
1130 to: "rule-deduplication",
1131 reason: "@value reachability must settle before rule deduplication",
1132 },
1133 TransformDagEdgeV0 {
1134 from: "tree-shake-custom-property",
1135 to: "rule-deduplication",
1136 reason: "custom-property reachability must settle before rule deduplication",
1137 },
1138 TransformDagEdgeV0 {
1139 from: "tree-shake-class",
1140 to: "empty-rule-removal",
1141 reason: "class tree shaking can leave ordinary and group rules empty",
1142 },
1143 TransformDagEdgeV0 {
1144 from: "tree-shake-keyframes",
1145 to: "empty-rule-removal",
1146 reason: "keyframe tree shaking can leave enclosing group rules empty",
1147 },
1148 TransformDagEdgeV0 {
1149 from: "tree-shake-value",
1150 to: "empty-rule-removal",
1151 reason: "@value tree shaking can leave module-only wrappers empty",
1152 },
1153 TransformDagEdgeV0 {
1154 from: "tree-shake-custom-property",
1155 to: "empty-rule-removal",
1156 reason: "custom-property tree shaking can leave declaration-only rules empty",
1157 },
1158 TransformDagEdgeV0 {
1159 from: "comment-strip",
1160 to: "empty-rule-removal",
1161 reason: "comment-only rules become removable empty rules after comment stripping",
1162 },
1163 TransformDagEdgeV0 {
1164 from: "light-dark-lowering",
1165 to: "vendor-prefixing",
1166 reason: "prefixing runs after target lowering produces final declarations",
1167 },
1168 TransformDagEdgeV0 {
1169 from: "color-mix-lowering",
1170 to: "vendor-prefixing",
1171 reason: "prefixing runs after target lowering produces final declarations",
1172 },
1173 TransformDagEdgeV0 {
1174 from: "oklch-oklab-lowering",
1175 to: "vendor-prefixing",
1176 reason: "prefixing runs after target lowering produces final declarations",
1177 },
1178 TransformDagEdgeV0 {
1179 from: "color-function-lowering",
1180 to: "vendor-prefixing",
1181 reason: "prefixing runs after target lowering produces final declarations",
1182 },
1183 TransformDagEdgeV0 {
1184 from: "logical-to-physical",
1185 to: "vendor-prefixing",
1186 reason: "prefixing runs after target lowering produces final declarations",
1187 },
1188 TransformDagEdgeV0 {
1189 from: "nesting-unwrap",
1190 to: "vendor-prefixing",
1191 reason: "prefixing runs after target lowering produces final declarations",
1192 },
1193 TransformDagEdgeV0 {
1194 from: "scope-flatten",
1195 to: "vendor-prefixing",
1196 reason: "prefixing runs after target lowering produces final declarations",
1197 },
1198 TransformDagEdgeV0 {
1199 from: "layer-flatten",
1200 to: "vendor-prefixing",
1201 reason: "prefixing runs after target lowering produces final declarations",
1202 },
1203 TransformDagEdgeV0 {
1204 from: "supports-static-eval",
1205 to: "vendor-prefixing",
1206 reason: "prefixing runs after target branch evaluation produces final declarations",
1207 },
1208 TransformDagEdgeV0 {
1209 from: "media-static-eval",
1210 to: "vendor-prefixing",
1211 reason: "prefixing runs after target branch evaluation produces final declarations",
1212 },
1213 TransformDagEdgeV0 {
1214 from: "calc-reduction",
1215 to: "print-css",
1216 reason: "printer consumes the final reduced transform CST",
1217 },
1218 TransformDagEdgeV0 {
1219 from: "whitespace-strip",
1220 to: "print-css",
1221 reason: "printer consumes the final trivia policy",
1222 },
1223 ]
1224}
1225
1226#[cfg(test)]
1227mod tests {
1228 use super::{
1229 StableTransformIrNodeKindV0, StyleDialect, TRANSFORM_PASS_CATALOG_LEN, TransformLayer,
1230 TransformPassKind, build_stable_transform_ir_from_source, build_transform_cst_artifact,
1231 summarize_omena_transform_cst_boundary,
1232 };
1233
1234 #[test]
1235 fn exposes_transform_cst_boundary_with_full_pass_catalog() {
1236 let boundary = summarize_omena_transform_cst_boundary();
1237
1238 assert_eq!(boundary.schema_version, "0");
1239 assert_eq!(boundary.product, "omena-transform-cst.boundary");
1240 assert_eq!(boundary.pass_catalog_count, TRANSFORM_PASS_CATALOG_LEN);
1241 assert!(boundary.full_pass_catalog_covered);
1242 assert_eq!(boundary.semantic_aware_pass_count, 14);
1243 assert_eq!(boundary.commodity_pass_count, 25);
1244 assert_eq!(boundary.emission_pass_count, 1);
1245 assert!(boundary.all_passes_declare_cascade_obligation);
1246 assert!(boundary.all_passes_have_compile_time_cascade_witness);
1247 assert!(boundary.stable_transform_ir_ready);
1248 assert!(boundary.provenance_derivation_forest_scaffold_ready);
1249 assert!(boundary.provenance_preservation_required);
1250 assert!(!boundary.next_surfaces.contains(&"omena-transform-passes"));
1251 assert!(!boundary.next_surfaces.contains(&"omena-transform-print"));
1252 assert!(!boundary.next_surfaces.contains(&"salsaTransformQueries"));
1253 assert!(!boundary.next_surfaces.contains(&"sourceMapSpanPrecision"));
1254 assert!(boundary.pass_contracts.iter().any(|contract| {
1255 contract.kind == TransformPassKind::TreeShakeClass
1256 && contract.label == "tree-shake-class"
1257 && contract.layer == TransformLayer::SemanticAware
1258 && contract.reads_semantic_graph
1259 && contract.cascade_safe
1260 && contract.cascade_safety_witness.pass_id == "tree-shake-class"
1261 && contract.cascade_safety_witness.enforced_at
1262 == "compile-time-exhaustive-pass-catalog"
1263 }));
1264 assert!(boundary.dag_edges.iter().any(|edge| {
1265 edge.from == "composes-resolution" && edge.to == "css-modules-class-hashing"
1266 }));
1267 }
1268
1269 #[test]
1270 fn transform_cst_artifact_preserves_semantic_signature_and_pass_ids() {
1271 let artifact = build_transform_cst_artifact(
1272 ".button { color: var(--brand); }",
1273 "semantic:button:brand",
1274 &[
1275 TransformPassKind::StaticVarSubstitution,
1276 TransformPassKind::ColorCompression,
1277 ],
1278 );
1279
1280 assert_eq!(artifact.product, "omena-transform-cst.artifact");
1281 assert_eq!(artifact.source_byte_len, 32);
1282 assert_eq!(artifact.semantic_signature, "semantic:button:brand");
1283 assert_eq!(artifact.stable_ir.product, "omena-transform-cst.stable-ir");
1284 assert_eq!(artifact.stable_ir.dialect, "css");
1285 assert_eq!(artifact.parser_error_count, 0);
1286 assert!(!artifact.contains_bogus_or_trivia);
1287 assert!(artifact.stable_ir.stable_post_semantic_ir);
1288 assert_eq!(
1289 artifact.stable_ir_node_count,
1290 artifact.stable_ir.provenance_anchors.len()
1291 );
1292 assert_eq!(
1293 artifact.pass_ids,
1294 vec!["custom-property-static-resolve", "color-compression"]
1295 );
1296 assert!(artifact.provenance_preserved);
1297 }
1298
1299 #[test]
1300 fn stable_transform_ir_consumes_parser_semantic_facts_without_trivia_or_bogus_nodes() {
1301 let ir = build_stable_transform_ir_from_source(
1302 r#"
1303@use "./tokens" as tokens;
1304@value primary from "./colors.module.css";
1305.button {
1306 composes: reset from "./reset.module.css";
1307 --brand: tokens.$brand;
1308 color: var(--brand);
1309}
1310"#,
1311 StyleDialect::Scss,
1312 "semantic:scss-button",
1313 );
1314
1315 assert_eq!(ir.product, "omena-transform-cst.stable-ir");
1316 assert_eq!(ir.dialect, "scss");
1317 assert_eq!(ir.parser_error_count, 0);
1318 assert!(!ir.contains_bogus_or_trivia);
1319 assert!(ir.stable_post_semantic_ir);
1320 assert_eq!(ir.node_count, ir.nodes.len());
1321 assert_eq!(ir.node_count, ir.provenance_anchors.len());
1322 assert!(ir.nodes.iter().any(|node| {
1323 node.kind == StableTransformIrNodeKindV0::ClassSelector && node.label == "button"
1324 }));
1325 assert!(ir.nodes.iter().any(|node| {
1326 node.kind == StableTransformIrNodeKindV0::CustomPropertyDeclaration
1327 && node.label == "--brand"
1328 }));
1329 assert!(ir.nodes.iter().any(|node| {
1330 node.kind == StableTransformIrNodeKindV0::CustomPropertyReference
1331 && node.label == "--brand"
1332 }));
1333 assert!(ir.nodes.iter().any(|node| {
1334 node.kind == StableTransformIrNodeKindV0::SassModuleEdge && node.label == "./tokens"
1335 }));
1336 assert!(
1337 ir.nodes
1338 .windows(2)
1339 .all(|pair| pair[0].source_span_start <= pair[1].source_span_start)
1340 );
1341 }
1342}