Skip to main content

omena_transform_cst/
lib.rs

1//! Transform CST contract substrate for the post-v5 omena-css track.
2//!
3//! This crate intentionally starts at the contract layer: transform passes are
4//! only valid when they declare which semantic/cascade facts they read and what
5//! cascade-safety obligation they must preserve.
6
7use 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}