1use serde::Serialize;
8
9#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Serialize)]
10#[serde(rename_all = "camelCase")]
11pub enum TransformLayer {
12 SemanticReadOnly,
13 SemanticAware,
14 Commodity,
15 Emission,
16}
17
18#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Serialize)]
19#[serde(rename_all = "camelCase")]
20pub enum TransformPassKind {
21 WhitespaceStrip,
22 CommentStrip,
23 NumberCompression,
24 UnitNormalization,
25 ColorCompression,
26 UrlQuoteStrip,
27 StringQuoteNormalize,
28 SelectorIsWhereCompression,
29 ShorthandCombining,
30 RuleDeduplication,
31 RuleMerging,
32 SelectorMerging,
33 EmptyRuleRemoval,
34 VendorPrefixing,
35 LightDarkLowering,
36 ColorMixLowering,
37 OklchOklabLowering,
38 ColorFunctionLowering,
39 LogicalToPhysical,
40 NestingUnwrap,
41 ScopeFlatten,
42 LayerFlatten,
43 SupportsStaticEval,
44 MediaStaticEval,
45 CalcReduction,
46 ImportInline,
47 ScssModuleEvaluate,
48 LessModuleEvaluate,
49 HashCssModuleClassNames,
50 ResolveCssModulesComposes,
51 ValueResolution,
52 StaticVarSubstitution,
53 TreeShakeClass,
54 TreeShakeKeyframes,
55 TreeShakeValue,
56 TreeShakeCustomProperty,
57 DeadMediaBranchRemoval,
58 DeadSupportsBranchRemoval,
59 DesignTokenRouting,
60 PrintCss,
61}
62
63pub const TRANSFORM_PASS_CATALOG_LEN: usize = 40;
64
65pub const fn all_transform_pass_kinds() -> [TransformPassKind; TRANSFORM_PASS_CATALOG_LEN] {
66 [
67 TransformPassKind::WhitespaceStrip,
68 TransformPassKind::CommentStrip,
69 TransformPassKind::NumberCompression,
70 TransformPassKind::UnitNormalization,
71 TransformPassKind::ColorCompression,
72 TransformPassKind::UrlQuoteStrip,
73 TransformPassKind::StringQuoteNormalize,
74 TransformPassKind::SelectorIsWhereCompression,
75 TransformPassKind::ShorthandCombining,
76 TransformPassKind::RuleDeduplication,
77 TransformPassKind::RuleMerging,
78 TransformPassKind::SelectorMerging,
79 TransformPassKind::EmptyRuleRemoval,
80 TransformPassKind::VendorPrefixing,
81 TransformPassKind::LightDarkLowering,
82 TransformPassKind::ColorMixLowering,
83 TransformPassKind::OklchOklabLowering,
84 TransformPassKind::ColorFunctionLowering,
85 TransformPassKind::LogicalToPhysical,
86 TransformPassKind::NestingUnwrap,
87 TransformPassKind::ScopeFlatten,
88 TransformPassKind::LayerFlatten,
89 TransformPassKind::SupportsStaticEval,
90 TransformPassKind::MediaStaticEval,
91 TransformPassKind::CalcReduction,
92 TransformPassKind::ImportInline,
93 TransformPassKind::ScssModuleEvaluate,
94 TransformPassKind::LessModuleEvaluate,
95 TransformPassKind::HashCssModuleClassNames,
96 TransformPassKind::ResolveCssModulesComposes,
97 TransformPassKind::ValueResolution,
98 TransformPassKind::StaticVarSubstitution,
99 TransformPassKind::TreeShakeClass,
100 TransformPassKind::TreeShakeKeyframes,
101 TransformPassKind::TreeShakeValue,
102 TransformPassKind::TreeShakeCustomProperty,
103 TransformPassKind::DeadMediaBranchRemoval,
104 TransformPassKind::DeadSupportsBranchRemoval,
105 TransformPassKind::DesignTokenRouting,
106 TransformPassKind::PrintCss,
107 ]
108}
109
110impl TransformPassKind {
111 pub const fn ordinal(self) -> u8 {
112 match self {
113 Self::WhitespaceStrip => 1,
114 Self::CommentStrip => 2,
115 Self::NumberCompression => 3,
116 Self::UnitNormalization => 4,
117 Self::ColorCompression => 5,
118 Self::UrlQuoteStrip => 6,
119 Self::StringQuoteNormalize => 7,
120 Self::SelectorIsWhereCompression => 8,
121 Self::ShorthandCombining => 9,
122 Self::RuleDeduplication => 10,
123 Self::RuleMerging => 11,
124 Self::SelectorMerging => 12,
125 Self::EmptyRuleRemoval => 13,
126 Self::VendorPrefixing => 14,
127 Self::LightDarkLowering => 15,
128 Self::ColorMixLowering => 16,
129 Self::OklchOklabLowering => 17,
130 Self::ColorFunctionLowering => 18,
131 Self::LogicalToPhysical => 19,
132 Self::NestingUnwrap => 20,
133 Self::ScopeFlatten => 21,
134 Self::LayerFlatten => 22,
135 Self::SupportsStaticEval => 23,
136 Self::MediaStaticEval => 24,
137 Self::CalcReduction => 25,
138 Self::ImportInline => 26,
139 Self::ScssModuleEvaluate => 27,
140 Self::LessModuleEvaluate => 28,
141 Self::HashCssModuleClassNames => 29,
142 Self::ResolveCssModulesComposes => 30,
143 Self::ValueResolution => 31,
144 Self::StaticVarSubstitution => 32,
145 Self::TreeShakeClass => 33,
146 Self::TreeShakeKeyframes => 34,
147 Self::TreeShakeValue => 35,
148 Self::TreeShakeCustomProperty => 36,
149 Self::DeadMediaBranchRemoval => 37,
150 Self::DeadSupportsBranchRemoval => 38,
151 Self::DesignTokenRouting => 39,
152 Self::PrintCss => 40,
153 }
154 }
155
156 pub const fn label(self) -> &'static str {
157 self.id()
158 }
159
160 pub const fn title(self) -> &'static str {
161 match self {
162 Self::WhitespaceStrip => "whitespace strip",
163 Self::CommentStrip => "comment strip",
164 Self::NumberCompression => "number compression",
165 Self::UnitNormalization => "unit normalization",
166 Self::ColorCompression => "color compression",
167 Self::UrlQuoteStrip => "url quote strip",
168 Self::StringQuoteNormalize => "string quote normalize",
169 Self::SelectorIsWhereCompression => "selector :is/:where compression",
170 Self::ShorthandCombining => "shorthand combining",
171 Self::RuleDeduplication => "rule deduplication",
172 Self::RuleMerging => "rule merging",
173 Self::SelectorMerging => "selector merging",
174 Self::EmptyRuleRemoval => "empty rule removal",
175 Self::VendorPrefixing => "vendor prefixing",
176 Self::LightDarkLowering => "light-dark lowering",
177 Self::ColorMixLowering => "color-mix lowering",
178 Self::OklchOklabLowering => "oklch/oklab lowering",
179 Self::ColorFunctionLowering => "color() lowering",
180 Self::LogicalToPhysical => "logical to physical",
181 Self::NestingUnwrap => "nesting unwrap",
182 Self::ScopeFlatten => "@scope flatten",
183 Self::LayerFlatten => "@layer flatten",
184 Self::SupportsStaticEval => "@supports static eval",
185 Self::MediaStaticEval => "@media static eval",
186 Self::CalcReduction => "calc() reduction",
187 Self::ImportInline => "@import inline",
188 Self::ScssModuleEvaluate => "SCSS module evaluate",
189 Self::LessModuleEvaluate => "Less module evaluate",
190 Self::HashCssModuleClassNames => "CSS Modules class hashing",
191 Self::ResolveCssModulesComposes => "composes resolution",
192 Self::ValueResolution => "@value resolution",
193 Self::StaticVarSubstitution => "custom property static resolve",
194 Self::TreeShakeClass => "tree shaking class",
195 Self::TreeShakeKeyframes => "tree shaking keyframes",
196 Self::TreeShakeValue => "tree shaking value",
197 Self::TreeShakeCustomProperty => "tree shaking custom-property",
198 Self::DeadMediaBranchRemoval => "dead @media branch removal",
199 Self::DeadSupportsBranchRemoval => "dead @supports branch removal",
200 Self::DesignTokenRouting => "design-token routing",
201 Self::PrintCss => "printer + sourcemap composer",
202 }
203 }
204
205 pub const fn id(self) -> &'static str {
206 match self {
207 Self::WhitespaceStrip => "whitespace-strip",
208 Self::CommentStrip => "comment-strip",
209 Self::NumberCompression => "number-compression",
210 Self::UnitNormalization => "unit-normalization",
211 Self::ColorCompression => "color-compression",
212 Self::UrlQuoteStrip => "url-quote-strip",
213 Self::StringQuoteNormalize => "string-quote-normalize",
214 Self::SelectorIsWhereCompression => "selector-is-where-compression",
215 Self::ShorthandCombining => "shorthand-combining",
216 Self::RuleDeduplication => "rule-deduplication",
217 Self::RuleMerging => "rule-merging",
218 Self::SelectorMerging => "selector-merging",
219 Self::EmptyRuleRemoval => "empty-rule-removal",
220 Self::VendorPrefixing => "vendor-prefixing",
221 Self::LightDarkLowering => "light-dark-lowering",
222 Self::ColorMixLowering => "color-mix-lowering",
223 Self::OklchOklabLowering => "oklch-oklab-lowering",
224 Self::ColorFunctionLowering => "color-function-lowering",
225 Self::LogicalToPhysical => "logical-to-physical",
226 Self::NestingUnwrap => "nesting-unwrap",
227 Self::ScopeFlatten => "scope-flatten",
228 Self::LayerFlatten => "layer-flatten",
229 Self::SupportsStaticEval => "supports-static-eval",
230 Self::MediaStaticEval => "media-static-eval",
231 Self::CalcReduction => "calc-reduction",
232 Self::ImportInline => "import-inline",
233 Self::ScssModuleEvaluate => "scss-module-evaluate",
234 Self::LessModuleEvaluate => "less-module-evaluate",
235 Self::HashCssModuleClassNames => "css-modules-class-hashing",
236 Self::ResolveCssModulesComposes => "composes-resolution",
237 Self::ValueResolution => "value-resolution",
238 Self::StaticVarSubstitution => "custom-property-static-resolve",
239 Self::TreeShakeClass => "tree-shake-class",
240 Self::TreeShakeKeyframes => "tree-shake-keyframes",
241 Self::TreeShakeValue => "tree-shake-value",
242 Self::TreeShakeCustomProperty => "tree-shake-custom-property",
243 Self::DeadMediaBranchRemoval => "dead-media-branch-removal",
244 Self::DeadSupportsBranchRemoval => "dead-supports-branch-removal",
245 Self::DesignTokenRouting => "design-token-routing",
246 Self::PrintCss => "print-css",
247 }
248 }
249
250 pub const fn layer(self) -> TransformLayer {
251 match self {
252 Self::ImportInline
253 | Self::ScssModuleEvaluate
254 | Self::LessModuleEvaluate
255 | Self::HashCssModuleClassNames
256 | Self::ResolveCssModulesComposes
257 | Self::ValueResolution
258 | Self::StaticVarSubstitution
259 | Self::TreeShakeClass
260 | Self::TreeShakeKeyframes
261 | Self::TreeShakeValue
262 | Self::TreeShakeCustomProperty
263 | Self::DeadMediaBranchRemoval
264 | Self::DeadSupportsBranchRemoval
265 | Self::DesignTokenRouting => TransformLayer::SemanticAware,
266 Self::PrintCss => TransformLayer::Emission,
267 _ => TransformLayer::Commodity,
268 }
269 }
270
271 pub const fn reads_semantic_graph(self) -> bool {
272 matches!(
273 self,
274 Self::ImportInline
275 | Self::ScssModuleEvaluate
276 | Self::LessModuleEvaluate
277 | Self::HashCssModuleClassNames
278 | Self::ResolveCssModulesComposes
279 | Self::ValueResolution
280 | Self::StaticVarSubstitution
281 | Self::TreeShakeClass
282 | Self::TreeShakeKeyframes
283 | Self::TreeShakeValue
284 | Self::TreeShakeCustomProperty
285 | Self::DeadMediaBranchRemoval
286 | Self::DeadSupportsBranchRemoval
287 | Self::DesignTokenRouting
288 )
289 }
290
291 pub const fn reads_cascade_model(self) -> bool {
292 matches!(
293 self,
294 Self::ShorthandCombining
295 | Self::RuleDeduplication
296 | Self::RuleMerging
297 | Self::SelectorMerging
298 | Self::ScopeFlatten
299 | Self::LayerFlatten
300 | Self::StaticVarSubstitution
301 | Self::DeadMediaBranchRemoval
302 | Self::DeadSupportsBranchRemoval
303 )
304 }
305
306 pub const fn read_model(self) -> TransformPassReadModel {
307 match self {
308 Self::VendorPrefixing
309 | Self::LightDarkLowering
310 | Self::ColorMixLowering
311 | Self::OklchOklabLowering
312 | Self::ColorFunctionLowering
313 | Self::LogicalToPhysical
314 | Self::NestingUnwrap => TransformPassReadModel::TargetData,
315 Self::ShorthandCombining
316 | Self::RuleDeduplication
317 | Self::RuleMerging
318 | Self::SelectorMerging
319 | Self::ScopeFlatten
320 | Self::LayerFlatten
321 | Self::StaticVarSubstitution
322 | Self::DeadMediaBranchRemoval
323 | Self::DeadSupportsBranchRemoval => TransformPassReadModel::CascadeModel,
324 Self::TreeShakeClass
325 | Self::TreeShakeKeyframes
326 | Self::TreeShakeValue
327 | Self::TreeShakeCustomProperty
328 | Self::DesignTokenRouting => TransformPassReadModel::BridgeReachability,
329 Self::ImportInline
330 | Self::ScssModuleEvaluate
331 | Self::LessModuleEvaluate
332 | Self::HashCssModuleClassNames
333 | Self::ResolveCssModulesComposes
334 | Self::ValueResolution => TransformPassReadModel::SemanticGraph,
335 Self::PrintCss => TransformPassReadModel::Emission,
336 _ => TransformPassReadModel::SyntaxOnly,
337 }
338 }
339}
340
341#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize)]
342#[serde(rename_all = "camelCase")]
343pub enum TransformPassReadModel {
344 SyntaxOnly,
345 TargetData,
346 CascadeModel,
347 SemanticGraph,
348 BridgeReachability,
349 Emission,
350}
351
352#[derive(Debug, Clone, PartialEq, Eq, Serialize)]
353#[serde(rename_all = "camelCase")]
354pub struct TransformPassContractV0 {
355 pub ordinal: u8,
356 pub label: &'static str,
357 pub id: &'static str,
358 pub title: &'static str,
359 pub kind: TransformPassKind,
360 pub layer: TransformLayer,
361 pub read_model: TransformPassReadModel,
362 pub reads_semantic_graph: bool,
363 pub reads_cascade_model: bool,
364 pub writes_css: bool,
365 pub cascade_safe_obligation: &'static str,
366}
367
368#[derive(Debug, Clone, PartialEq, Eq, Serialize)]
369#[serde(rename_all = "camelCase")]
370pub struct TransformDagEdgeV0 {
371 pub from: &'static str,
372 pub to: &'static str,
373 pub reason: &'static str,
374}
375
376#[derive(Debug, Clone, PartialEq, Eq, Serialize)]
377#[serde(rename_all = "camelCase")]
378pub struct TransformCstBoundarySummaryV0 {
379 pub schema_version: &'static str,
380 pub product: &'static str,
381 pub representation: &'static str,
382 pub pass_contracts: Vec<TransformPassContractV0>,
383 pub dag_edges: Vec<TransformDagEdgeV0>,
384 pub pass_catalog_count: usize,
385 pub semantic_aware_pass_count: usize,
386 pub commodity_pass_count: usize,
387 pub emission_pass_count: usize,
388 pub full_pass_catalog_covered: bool,
389 pub all_passes_declare_cascade_obligation: bool,
390 pub provenance_preservation_required: bool,
391 pub next_surfaces: Vec<&'static str>,
392}
393
394#[derive(Debug, Clone, PartialEq, Eq, Serialize)]
395#[serde(rename_all = "camelCase")]
396pub struct TransformCstArtifactV0 {
397 pub schema_version: &'static str,
398 pub product: &'static str,
399 pub source_byte_len: usize,
400 pub semantic_signature: String,
401 pub pass_ids: Vec<&'static str>,
402 pub provenance_preserved: bool,
403}
404
405pub fn summarize_omena_transform_cst_boundary() -> TransformCstBoundarySummaryV0 {
406 let pass_contracts = default_transform_pass_contracts();
407 let semantic_aware_pass_count = pass_contracts
408 .iter()
409 .filter(|contract| contract.layer == TransformLayer::SemanticAware)
410 .count();
411 let commodity_pass_count = pass_contracts
412 .iter()
413 .filter(|contract| contract.layer == TransformLayer::Commodity)
414 .count();
415 let emission_pass_count = pass_contracts
416 .iter()
417 .filter(|contract| contract.layer == TransformLayer::Emission)
418 .count();
419 let all_passes_declare_cascade_obligation = pass_contracts
420 .iter()
421 .all(|contract| !contract.cascade_safe_obligation.is_empty());
422 let pass_catalog_count = pass_contracts.len();
423
424 TransformCstBoundarySummaryV0 {
425 schema_version: "0",
426 product: "omena-transform-cst.boundary",
427 representation: "post-semantic-provenance-preserving-transform-cst",
428 pass_contracts,
429 dag_edges: default_transform_dag_edges(),
430 pass_catalog_count,
431 semantic_aware_pass_count,
432 commodity_pass_count,
433 emission_pass_count,
434 full_pass_catalog_covered: pass_catalog_count == TRANSFORM_PASS_CATALOG_LEN,
435 all_passes_declare_cascade_obligation,
436 provenance_preservation_required: true,
437 next_surfaces: Vec::new(),
438 }
439}
440
441pub fn build_transform_cst_artifact(
442 source: &str,
443 semantic_signature: impl Into<String>,
444 passes: &[TransformPassKind],
445) -> TransformCstArtifactV0 {
446 TransformCstArtifactV0 {
447 schema_version: "0",
448 product: "omena-transform-cst.artifact",
449 source_byte_len: source.len(),
450 semantic_signature: semantic_signature.into(),
451 pass_ids: passes.iter().map(|pass| pass.id()).collect(),
452 provenance_preserved: true,
453 }
454}
455
456pub fn default_transform_pass_contracts() -> Vec<TransformPassContractV0> {
457 all_transform_pass_kinds()
458 .into_iter()
459 .map(transform_pass_contract)
460 .collect()
461}
462
463fn transform_pass_contract(kind: TransformPassKind) -> TransformPassContractV0 {
464 TransformPassContractV0 {
465 ordinal: kind.ordinal(),
466 label: kind.label(),
467 id: kind.id(),
468 title: kind.title(),
469 kind,
470 layer: kind.layer(),
471 read_model: kind.read_model(),
472 reads_semantic_graph: kind.reads_semantic_graph(),
473 reads_cascade_model: kind.reads_cascade_model(),
474 writes_css: true,
475 cascade_safe_obligation: cascade_safe_obligation(kind),
476 }
477}
478
479fn cascade_safe_obligation(kind: TransformPassKind) -> &'static str {
480 match kind {
481 TransformPassKind::WhitespaceStrip => {
482 "may remove only whitespace outside string, url, attr, and calc-sensitive token boundaries"
483 }
484 TransformPassKind::CommentStrip => {
485 "may remove comments only when source-map provenance preserves the removed span"
486 }
487 TransformPassKind::NumberCompression => {
488 "may rewrite only numerically equivalent literal tokens"
489 }
490 TransformPassKind::UnitNormalization => {
491 "may normalize only dimension values whose computed value is unchanged"
492 }
493 TransformPassKind::ColorCompression => "may rewrite only color-equivalent literal tokens",
494 TransformPassKind::UrlQuoteStrip => {
495 "may remove url quotes only when the unquoted token grammar remains equivalent"
496 }
497 TransformPassKind::StringQuoteNormalize => {
498 "may normalize string quotes only when escaped contents remain byte-equivalent after decoding"
499 }
500 TransformPassKind::SelectorIsWhereCompression => {
501 "must preserve selector specificity and matching semantics under the cascade model"
502 }
503 TransformPassKind::ShorthandCombining => {
504 "must prove longhand and shorthand cascade outcomes are equivalent"
505 }
506 TransformPassKind::RuleDeduplication => {
507 "must preserve origin, layer, specificity, and order for every surviving declaration"
508 }
509 TransformPassKind::RuleMerging => {
510 "must prove merged rule order cannot change declaration winners"
511 }
512 TransformPassKind::SelectorMerging => {
513 "must preserve selector identity and post-hash module semantics"
514 }
515 TransformPassKind::EmptyRuleRemoval => {
516 "may remove rules only when no source-visible semantic marker is attached"
517 }
518 TransformPassKind::VendorPrefixing => {
519 "must add target-required prefixed declarations without changing modern target outcomes"
520 }
521 TransformPassKind::LightDarkLowering => {
522 "must lower only when target data requires fallback branches and provenance tracks both branches"
523 }
524 TransformPassKind::ColorMixLowering => {
525 "must lower only when color-space conversion is target-equivalent"
526 }
527 TransformPassKind::OklchOklabLowering => {
528 "must preserve color semantics within the configured target fallback precision"
529 }
530 TransformPassKind::ColorFunctionLowering => {
531 "must preserve color semantics within the configured target fallback precision"
532 }
533 TransformPassKind::LogicalToPhysical => {
534 "must run only under explicit directionality options"
535 }
536 TransformPassKind::NestingUnwrap => {
537 "must preserve nested selector expansion and specificity"
538 }
539 TransformPassKind::ScopeFlatten => {
540 "must preserve scoped matching semantics or emit a blocked result"
541 }
542 TransformPassKind::LayerFlatten => "must preserve layer order in CascadeKey comparison",
543 TransformPassKind::SupportsStaticEval => {
544 "may remove branches only when the target feature predicate is known"
545 }
546 TransformPassKind::MediaStaticEval => {
547 "may remove branches only when the configured media predicate is known"
548 }
549 TransformPassKind::CalcReduction => {
550 "may reduce only syntax-equivalent or computed-value-equivalent calc expressions"
551 }
552 TransformPassKind::ImportInline => {
553 "must preserve import-site media, supports, layer wrappers, and source provenance"
554 }
555 TransformPassKind::ScssModuleEvaluate => {
556 "must preserve SCSS namespace, show/hide, mixin, variable, and source provenance facts"
557 }
558 TransformPassKind::LessModuleEvaluate => {
559 "must preserve Less variable, mixin, namespace, and source provenance facts"
560 }
561 TransformPassKind::HashCssModuleClassNames => {
562 "must rewrite every source and style reference through the same selector identity map"
563 }
564 TransformPassKind::ResolveCssModulesComposes => {
565 "must preserve exported class set and composed class provenance"
566 }
567 TransformPassKind::ValueResolution => {
568 "must preserve @value graph resolution and cycle diagnostics"
569 }
570 TransformPassKind::StaticVarSubstitution => {
571 "must preserve custom-property fixed-point semantics or emit a provenance-backed blocked result"
572 }
573 TransformPassKind::TreeShakeClass => {
574 "may remove classes only when bridge reachability proves no reachable source expression observes them"
575 }
576 TransformPassKind::TreeShakeKeyframes => {
577 "may remove keyframes only when animation-name reachability proves they are unobservable"
578 }
579 TransformPassKind::TreeShakeValue => {
580 "may remove @value declarations only when value-graph traversal proves they are unreachable"
581 }
582 TransformPassKind::TreeShakeCustomProperty => {
583 "may remove custom properties only when var() reachability proves they are unobservable"
584 }
585 TransformPassKind::DeadMediaBranchRemoval => {
586 "may remove @media branches only when target and cascade witnesses prove deadness"
587 }
588 TransformPassKind::DeadSupportsBranchRemoval => {
589 "may remove @supports branches only when target and cascade witnesses prove deadness"
590 }
591 TransformPassKind::DesignTokenRouting => {
592 "must preserve design-token provenance while routing declarations across package boundaries"
593 }
594 TransformPassKind::PrintCss => {
595 "must emit a source-map trace for every non-trivia transformed span"
596 }
597 }
598}
599
600pub fn default_transform_dag_edges() -> Vec<TransformDagEdgeV0> {
601 vec![
602 TransformDagEdgeV0 {
603 from: "import-inline",
604 to: "custom-property-static-resolve",
605 reason: "var() resolution needs the full custom-property graph from inlined files",
606 },
607 TransformDagEdgeV0 {
608 from: "scss-module-evaluate",
609 to: "custom-property-static-resolve",
610 reason: "SCSS evaluation can introduce custom-property declarations",
611 },
612 TransformDagEdgeV0 {
613 from: "less-module-evaluate",
614 to: "custom-property-static-resolve",
615 reason: "Less evaluation can introduce custom-property declarations",
616 },
617 TransformDagEdgeV0 {
618 from: "composes-resolution",
619 to: "css-modules-class-hashing",
620 reason: "hashing must run after composed class expansion",
621 },
622 TransformDagEdgeV0 {
623 from: "css-modules-class-hashing",
624 to: "selector-merging",
625 reason: "selector merging must see post-hash selector identities",
626 },
627 TransformDagEdgeV0 {
628 from: "custom-property-static-resolve",
629 to: "calc-reduction",
630 reason: "var() inside calc may resolve to numeric literals that enable reduction",
631 },
632 TransformDagEdgeV0 {
633 from: "tree-shake-class",
634 to: "rule-deduplication",
635 reason: "tree shaking must run before rule deduplication can hide dead rules",
636 },
637 TransformDagEdgeV0 {
638 from: "tree-shake-keyframes",
639 to: "rule-deduplication",
640 reason: "keyframe reachability must settle before rule deduplication",
641 },
642 TransformDagEdgeV0 {
643 from: "tree-shake-value",
644 to: "rule-deduplication",
645 reason: "@value reachability must settle before rule deduplication",
646 },
647 TransformDagEdgeV0 {
648 from: "tree-shake-custom-property",
649 to: "rule-deduplication",
650 reason: "custom-property reachability must settle before rule deduplication",
651 },
652 TransformDagEdgeV0 {
653 from: "light-dark-lowering",
654 to: "vendor-prefixing",
655 reason: "prefixing runs after target lowering produces final declarations",
656 },
657 TransformDagEdgeV0 {
658 from: "color-mix-lowering",
659 to: "vendor-prefixing",
660 reason: "prefixing runs after target lowering produces final declarations",
661 },
662 TransformDagEdgeV0 {
663 from: "oklch-oklab-lowering",
664 to: "vendor-prefixing",
665 reason: "prefixing runs after target lowering produces final declarations",
666 },
667 TransformDagEdgeV0 {
668 from: "color-function-lowering",
669 to: "vendor-prefixing",
670 reason: "prefixing runs after target lowering produces final declarations",
671 },
672 TransformDagEdgeV0 {
673 from: "logical-to-physical",
674 to: "vendor-prefixing",
675 reason: "prefixing runs after target lowering produces final declarations",
676 },
677 TransformDagEdgeV0 {
678 from: "nesting-unwrap",
679 to: "vendor-prefixing",
680 reason: "prefixing runs after target lowering produces final declarations",
681 },
682 TransformDagEdgeV0 {
683 from: "scope-flatten",
684 to: "vendor-prefixing",
685 reason: "prefixing runs after target lowering produces final declarations",
686 },
687 TransformDagEdgeV0 {
688 from: "layer-flatten",
689 to: "vendor-prefixing",
690 reason: "prefixing runs after target lowering produces final declarations",
691 },
692 TransformDagEdgeV0 {
693 from: "supports-static-eval",
694 to: "vendor-prefixing",
695 reason: "prefixing runs after target branch evaluation produces final declarations",
696 },
697 TransformDagEdgeV0 {
698 from: "media-static-eval",
699 to: "vendor-prefixing",
700 reason: "prefixing runs after target branch evaluation produces final declarations",
701 },
702 TransformDagEdgeV0 {
703 from: "calc-reduction",
704 to: "print-css",
705 reason: "printer consumes the final reduced transform CST",
706 },
707 TransformDagEdgeV0 {
708 from: "whitespace-strip",
709 to: "print-css",
710 reason: "printer consumes the final trivia policy",
711 },
712 ]
713}
714
715#[cfg(test)]
716mod tests {
717 use super::{
718 TRANSFORM_PASS_CATALOG_LEN, TransformLayer, TransformPassKind,
719 build_transform_cst_artifact, summarize_omena_transform_cst_boundary,
720 };
721
722 #[test]
723 fn exposes_transform_cst_boundary_with_full_pass_catalog() {
724 let boundary = summarize_omena_transform_cst_boundary();
725
726 assert_eq!(boundary.schema_version, "0");
727 assert_eq!(boundary.product, "omena-transform-cst.boundary");
728 assert_eq!(boundary.pass_catalog_count, TRANSFORM_PASS_CATALOG_LEN);
729 assert!(boundary.full_pass_catalog_covered);
730 assert_eq!(boundary.semantic_aware_pass_count, 14);
731 assert_eq!(boundary.commodity_pass_count, 25);
732 assert_eq!(boundary.emission_pass_count, 1);
733 assert!(boundary.all_passes_declare_cascade_obligation);
734 assert!(boundary.provenance_preservation_required);
735 assert!(!boundary.next_surfaces.contains(&"omena-transform-passes"));
736 assert!(!boundary.next_surfaces.contains(&"omena-transform-print"));
737 assert!(!boundary.next_surfaces.contains(&"salsaTransformQueries"));
738 assert!(!boundary.next_surfaces.contains(&"sourceMapSpanPrecision"));
739 assert!(boundary.pass_contracts.iter().any(|contract| {
740 contract.kind == TransformPassKind::TreeShakeClass
741 && contract.label == "tree-shake-class"
742 && contract.layer == TransformLayer::SemanticAware
743 && contract.reads_semantic_graph
744 }));
745 assert!(boundary.dag_edges.iter().any(|edge| {
746 edge.from == "composes-resolution" && edge.to == "css-modules-class-hashing"
747 }));
748 }
749
750 #[test]
751 fn transform_cst_artifact_preserves_semantic_signature_and_pass_ids() {
752 let artifact = build_transform_cst_artifact(
753 ".button { color: var(--brand); }",
754 "semantic:button:brand",
755 &[
756 TransformPassKind::StaticVarSubstitution,
757 TransformPassKind::ColorCompression,
758 ],
759 );
760
761 assert_eq!(artifact.product, "omena-transform-cst.artifact");
762 assert_eq!(artifact.source_byte_len, 32);
763 assert_eq!(artifact.semantic_signature, "semantic:button:brand");
764 assert_eq!(
765 artifact.pass_ids,
766 vec!["custom-property-static-resolve", "color-compression"]
767 );
768 assert!(artifact.provenance_preserved);
769 }
770}