Skip to main content

omena_bundler/
lib.rs

1//! Standalone 0.x bundle planning for Omena CSS transforms.
2//!
3//! This crate is the standalone Rust entry point for the Omena bundler planning
4//! surface. It decides which bundle/module passes are required for a style
5//! source and delegates ordering to `omena-transform-passes`.
6//!
7//! The public types intentionally keep their `V0` suffix during the 0.x line.
8
9#[cfg(test)]
10mod carrier_hygiene_assertions;
11mod emission_items;
12mod emission_order;
13
14pub use emission_items::{
15    EmissionItemFactCategoryV0, EmissionItemInputV0, EmissionItemKindV0, EmissionItemOrderKeyV0,
16    EmissionItemPlanV0, EmissionItemProjectionDisclosureV0, EmissionItemProjectionDispositionV0,
17    EmissionItemProjectionReasonV0, EmissionItemV0, LinkedEmissionItemMaterializationErrorV0,
18    LinkedEmissionItemOrderV0, LinkedEmissionItemV0, TransformBundleEmissionItemProjectionV0,
19};
20pub use emission_order::{
21    EmissionCycleClassV0, EmissionCycleGroupV0, EmissionCyclePolicyV0, EmissionDependencyFactV0,
22    EmissionOrderKeyV0, EmissionOrderingPolicyV0, EmissionPlanV0,
23};
24
25use omena_cascade::{
26    CascadeKey, CascadeLevel, LayerOrdinal, ModuleRank, OpenWorldTieEvidence, Specificity,
27    normalized_layer_rank,
28};
29use omena_cross_file_summary::{EdgeOrderRelevanceV0, OmenaCrossFileSummaryRawEdgeKindV0};
30use omena_parser::{
31    ClosedWorldBundleBuildErrorV0, ClosedWorldBundleV0, ClosedWorldComposesEdgeV0,
32    ClosedWorldLinkedModuleV0, ClosedWorldModuleMetadataV0,
33    ClosedWorldModuleReachabilityEvidenceV0, ConfigurationHashV0, ModuleIdV0, ModuleInstanceKeyV0,
34    ParsedAnimationFactKind, ParsedCssModuleComposesEdgeKind, ParsedCssModuleValueFactKind,
35    ParsedEmissionSelectorFactsV0, ParsedSassModuleEdgeFactKind, ParsedSelectorFactKind,
36    ParsedStyleFacts, ParsedVariableFactKind, StyleDialect, collect_style_fact_collection,
37    collect_style_facts,
38};
39use omena_transform_cst::{
40    IrNodeKindV0, TransformPassKind, lower_transform_ir_from_source, transform_pass_sort_ordinal,
41};
42use omena_transform_passes::{TransformPassPlanV0, plan_transform_passes};
43use serde::Serialize;
44use std::{
45    collections::{BTreeMap, BTreeSet},
46    path::{Component, Path, PathBuf},
47};
48
49#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize)]
50#[serde(rename_all = "camelCase")]
51pub enum TransformBundleEdgeKind {
52    SassUse,
53    SassForward,
54    SassImport,
55    CssImport,
56    LessImport,
57    CssModuleValueImport,
58    CssModuleComposesLocal,
59    CssModuleComposesExternal,
60    IcssImport,
61}
62
63pub const TRANSFORM_BUNDLE_EDGE_KIND_VARIANTS_V0: [TransformBundleEdgeKind; 9] = [
64    TransformBundleEdgeKind::SassUse,
65    TransformBundleEdgeKind::SassForward,
66    TransformBundleEdgeKind::SassImport,
67    TransformBundleEdgeKind::CssImport,
68    TransformBundleEdgeKind::LessImport,
69    TransformBundleEdgeKind::CssModuleValueImport,
70    TransformBundleEdgeKind::CssModuleComposesLocal,
71    TransformBundleEdgeKind::CssModuleComposesExternal,
72    TransformBundleEdgeKind::IcssImport,
73];
74
75impl TransformBundleEdgeKind {
76    pub const fn as_wire_label(self) -> &'static str {
77        match self {
78            Self::SassUse => "sassUse",
79            Self::SassForward => "sassForward",
80            Self::SassImport => "sassImport",
81            Self::CssImport => "cssImport",
82            Self::LessImport => "lessImport",
83            Self::CssModuleValueImport => "cssModuleValueImport",
84            Self::CssModuleComposesLocal => "cssModuleComposesLocal",
85            Self::CssModuleComposesExternal => "cssModuleComposesExternal",
86            Self::IcssImport => "icssImport",
87        }
88    }
89
90    pub const fn order_relevance(self) -> EdgeOrderRelevanceV0 {
91        self.raw_edge_kind().order_relevance()
92    }
93
94    pub const fn order_relevance_reason(self) -> &'static str {
95        match self {
96            Self::SassUse => "Sass module use sequence participates in evaluation order",
97            Self::SassForward => "Sass forwarding sequence participates in module exposure order",
98            Self::SassImport => "Sass import sequence participates in emitted rule order",
99            Self::CssImport => "CSS import sequence participates in emitted rule order",
100            Self::LessImport => "Less import sequence participates in evaluation order",
101            Self::CssModuleValueImport => {
102                "CSS Modules value imports participate in dependency evaluation order"
103            }
104            Self::CssModuleComposesLocal => "local composition preserves selector dependency order",
105            Self::CssModuleComposesExternal => {
106                "external composition preserves module dependency order"
107            }
108            Self::IcssImport => "ICSS imports participate in dependency evaluation order",
109        }
110    }
111
112    const fn raw_edge_kind(self) -> OmenaCrossFileSummaryRawEdgeKindV0 {
113        match self {
114            Self::SassUse => OmenaCrossFileSummaryRawEdgeKindV0::SassUse,
115            Self::SassForward => OmenaCrossFileSummaryRawEdgeKindV0::SassForward,
116            Self::SassImport => OmenaCrossFileSummaryRawEdgeKindV0::SassImport,
117            Self::CssImport => OmenaCrossFileSummaryRawEdgeKindV0::CssModulesImport,
118            Self::LessImport => OmenaCrossFileSummaryRawEdgeKindV0::LessImport,
119            Self::CssModuleValueImport => OmenaCrossFileSummaryRawEdgeKindV0::CssModulesValueImport,
120            Self::CssModuleComposesLocal => OmenaCrossFileSummaryRawEdgeKindV0::ComposesLocal,
121            Self::CssModuleComposesExternal => OmenaCrossFileSummaryRawEdgeKindV0::ComposesExternal,
122            Self::IcssImport => OmenaCrossFileSummaryRawEdgeKindV0::CssModulesIcssImport,
123        }
124    }
125}
126
127#[derive(Debug, Clone, PartialEq, Eq, Serialize)]
128#[serde(rename_all = "camelCase")]
129pub struct TransformBundleEdgeV0 {
130    pub kind: TransformBundleEdgeKind,
131    pub source_path: String,
132    pub import_source: Option<String>,
133    #[serde(skip_serializing_if = "Option::is_none")]
134    pub import_ordinal: Option<u32>,
135    pub namespace: Option<String>,
136    pub local_names: Vec<String>,
137    pub remote_names: Vec<String>,
138    pub range_start: u32,
139    pub range_end: u32,
140    pub provenance_required: bool,
141}
142
143#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize)]
144#[serde(rename_all = "camelCase")]
145pub enum TransformBundleAssetUrlKind {
146    Relative,
147    AbsolutePath,
148    External,
149    Data,
150    Fragment,
151}
152
153#[derive(Debug, Clone, PartialEq, Eq, Serialize)]
154#[serde(rename_all = "camelCase")]
155pub struct TransformBundleAssetUrlV0 {
156    pub source_path: String,
157    pub raw_url: String,
158    pub normalized_url: String,
159    pub kind: TransformBundleAssetUrlKind,
160    pub resolved_path: Option<String>,
161    pub range_start: u32,
162    pub range_end: u32,
163    pub bundler_resolution_required: bool,
164}
165
166#[derive(Debug, Clone, PartialEq, Eq, Serialize)]
167#[serde(rename_all = "camelCase")]
168pub struct TransformBundleAssetUrlRewriteSummaryV0 {
169    pub schema_version: &'static str,
170    pub product: &'static str,
171    pub source_path: String,
172    pub asset_url_count: usize,
173    pub rewrite_count: usize,
174    pub output_css: String,
175    pub rewritten_asset_urls: Vec<TransformBundleAssetUrlV0>,
176}
177
178#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize)]
179#[serde(rename_all = "camelCase")]
180pub enum TransformBundleChunkKind {
181    Entry,
182    StyleImport,
183    Asset,
184}
185
186#[derive(Debug, Clone, PartialEq, Eq, Serialize)]
187#[serde(rename_all = "camelCase")]
188pub struct TransformBundleChunkV0 {
189    pub chunk_id: String,
190    pub kind: TransformBundleChunkKind,
191    pub source_path: String,
192    pub import_source: Option<String>,
193    pub asset_url: Option<String>,
194    pub resolved_path: Option<String>,
195    pub depends_on: Vec<String>,
196    pub split_boundary: &'static str,
197}
198
199#[derive(Debug, Clone, PartialEq, Eq, Serialize)]
200#[serde(rename_all = "camelCase")]
201pub struct TransformBundleSourceSummaryV0 {
202    pub schema_version: &'static str,
203    pub product: &'static str,
204    pub source_path: String,
205    pub dialect: &'static str,
206    pub bundle_edges: Vec<TransformBundleEdgeV0>,
207    pub asset_urls: Vec<TransformBundleAssetUrlV0>,
208    pub code_split_chunks: Vec<TransformBundleChunkV0>,
209    pub required_pass_ids: Vec<&'static str>,
210    pub planned_pass_ids: Vec<&'static str>,
211    pub import_inline_required: bool,
212    pub module_evaluation_required: bool,
213    pub css_modules_resolution_required: bool,
214    pub class_hashing_required: bool,
215    pub value_resolution_required: bool,
216    pub code_splitting_required: bool,
217    pub pass_plan: TransformPassPlanV0,
218}
219
220#[derive(Debug, Clone, PartialEq, Eq)]
221pub struct TransformBundleModuleInputV0 {
222    pub source_path: String,
223    pub source: String,
224    pub dialect: StyleDialect,
225    pub configuration_hash: ConfigurationHashV0,
226}
227
228impl TransformBundleModuleInputV0 {
229    pub fn new(
230        source_path: impl Into<String>,
231        source: impl Into<String>,
232        dialect: StyleDialect,
233    ) -> Self {
234        Self {
235            source_path: source_path.into(),
236            source: source.into(),
237            dialect,
238            configuration_hash: ConfigurationHashV0::none(),
239        }
240    }
241
242    pub fn with_configuration_hash(mut self, configuration_hash: ConfigurationHashV0) -> Self {
243        self.configuration_hash = configuration_hash;
244        self
245    }
246
247    pub fn module_instance_key(&self) -> ModuleInstanceKeyV0 {
248        ModuleInstanceKeyV0::new(
249            ModuleIdV0::new(normalize_bundle_path(PathBuf::from(&self.source_path))),
250            self.configuration_hash.clone(),
251        )
252    }
253}
254
255#[non_exhaustive]
256#[derive(Debug, Clone, PartialEq, Eq)]
257pub struct TransformBundleParsedModuleInputV0 {
258    source_path: String,
259    dialect: StyleDialect,
260    facts: ParsedStyleFacts,
261    emission_selectors: ParsedEmissionSelectorFactsV0,
262    configuration_hashes: Vec<ConfigurationHashV0>,
263}
264
265impl TransformBundleParsedModuleInputV0 {
266    pub fn new(
267        source_path: impl Into<String>,
268        dialect: StyleDialect,
269        facts: ParsedStyleFacts,
270    ) -> Self {
271        Self {
272            source_path: source_path.into(),
273            dialect,
274            facts,
275            emission_selectors: ParsedEmissionSelectorFactsV0::default(),
276            configuration_hashes: vec![ConfigurationHashV0::none()],
277        }
278    }
279
280    pub fn with_emission_selectors(
281        mut self,
282        emission_selectors: ParsedEmissionSelectorFactsV0,
283    ) -> Self {
284        self.emission_selectors = emission_selectors;
285        self
286    }
287
288    pub fn with_configuration_hashes(
289        mut self,
290        configuration_hashes: Vec<ConfigurationHashV0>,
291    ) -> Self {
292        self.configuration_hashes = configuration_hashes
293            .into_iter()
294            .collect::<BTreeSet<_>>()
295            .into_iter()
296            .collect();
297        if self.configuration_hashes.is_empty() {
298            self.configuration_hashes.push(ConfigurationHashV0::none());
299        }
300        self
301    }
302
303    pub fn source_path(&self) -> &str {
304        self.source_path.as_str()
305    }
306
307    pub fn configuration_hashes(&self) -> &[ConfigurationHashV0] {
308        self.configuration_hashes.as_slice()
309    }
310
311    pub fn module_instance_keys(&self) -> Vec<ModuleInstanceKeyV0> {
312        let module = ModuleIdV0::new(normalize_bundle_path(PathBuf::from(&self.source_path)));
313        self.configuration_hashes
314            .iter()
315            .cloned()
316            .map(|configuration| ModuleInstanceKeyV0::new(module.clone(), configuration))
317            .collect()
318    }
319}
320
321#[deprecated(
322    note = "use TransformBundleInstanceReachabilityInputV0 so the module instance and derivation are explicit"
323)]
324#[derive(Debug, Clone, Default, PartialEq, Eq)]
325pub struct TransformBundleSemanticReachabilityInputV0 {
326    pub source_path: String,
327    pub class_names: Vec<String>,
328    pub keyframe_names: Vec<String>,
329    pub value_names: Vec<String>,
330    pub custom_property_names: Vec<String>,
331}
332
333#[non_exhaustive]
334#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize)]
335#[serde(rename_all = "camelCase")]
336pub enum InstanceReachabilityDerivationV0 {
337    /// Reserved for a producer that can distinguish configured module instances.
338    ///
339    /// InstanceAttributed remains unproduced; path-union reachability is a disclosed over-approximation.
340    InstanceAttributed,
341    PathUnionNoInstanceDiscriminator,
342}
343
344#[non_exhaustive]
345#[derive(Debug, Clone, PartialEq, Eq)]
346pub struct TransformBundleInstanceReachabilityInputV0 {
347    pub module_instance: ModuleInstanceKeyV0,
348    pub class_names: Vec<String>,
349    pub keyframe_names: Vec<String>,
350    pub value_names: Vec<String>,
351    pub custom_property_names: Vec<String>,
352    pub derivation: InstanceReachabilityDerivationV0,
353}
354
355impl TransformBundleInstanceReachabilityInputV0 {
356    pub fn new(
357        module_instance: ModuleInstanceKeyV0,
358        derivation: InstanceReachabilityDerivationV0,
359    ) -> Self {
360        Self {
361            module_instance,
362            class_names: Vec::new(),
363            keyframe_names: Vec::new(),
364            value_names: Vec::new(),
365            custom_property_names: Vec::new(),
366            derivation,
367        }
368    }
369
370    pub fn has_reachable_symbols(&self) -> bool {
371        !self.class_names.is_empty()
372            || !self.keyframe_names.is_empty()
373            || !self.value_names.is_empty()
374            || !self.custom_property_names.is_empty()
375    }
376}
377
378#[allow(deprecated)]
379impl TransformBundleSemanticReachabilityInputV0 {
380    pub fn new(source_path: impl Into<String>) -> Self {
381        Self {
382            source_path: source_path.into(),
383            ..Self::default()
384        }
385    }
386
387    pub fn has_reachable_symbols(&self) -> bool {
388        !self.class_names.is_empty()
389            || !self.keyframe_names.is_empty()
390            || !self.value_names.is_empty()
391            || !self.custom_property_names.is_empty()
392    }
393}
394
395#[derive(Debug, Clone, PartialEq, Eq, Serialize)]
396#[serde(rename_all = "camelCase")]
397pub struct LinkerDependencyEdgeV0 {
398    pub kind: TransformBundleEdgeKind,
399    pub import_source: String,
400    pub import_ordinal: Option<u32>,
401    pub local_names: Vec<String>,
402    pub remote_names: Vec<String>,
403}
404
405#[derive(Debug, Clone, PartialEq, Eq, Serialize)]
406#[serde(rename_all = "camelCase")]
407pub struct LinkerRuleV0 {
408    pub selector_name: String,
409    #[serde(serialize_with = "serialize_selector_fact_kind")]
410    pub selector_kind: ParsedSelectorFactKind,
411    pub range_start: u32,
412    pub range_end: u32,
413}
414
415#[derive(Debug, Clone, PartialEq, Eq, Serialize)]
416#[serde(rename_all = "camelCase")]
417pub struct LinkerInputV0 {
418    pub source_path: String,
419    pub instance: ModuleInstanceKeyV0,
420    pub dependency_edges: Vec<LinkerDependencyEdgeV0>,
421    pub class_names: Vec<String>,
422    pub keyframe_names: Vec<String>,
423    pub value_names: Vec<String>,
424    pub custom_property_names: Vec<String>,
425    pub ordered_rules: Vec<LinkerRuleV0>,
426}
427
428#[non_exhaustive]
429#[derive(Debug, Clone, PartialEq, Eq)]
430pub struct TransformBundleLinkerProjectionV0 {
431    inputs: Vec<LinkerInputV0>,
432    module_reachability_evidence:
433        BTreeMap<ModuleInstanceKeyV0, ClosedWorldModuleReachabilityEvidenceV0>,
434    module_reachability_derivations:
435        BTreeMap<ModuleInstanceKeyV0, InstanceReachabilityDerivationV0>,
436}
437
438#[non_exhaustive]
439#[derive(Debug, Clone, PartialEq, Eq)]
440pub struct TransformBundleLinkProjectionSetV0 {
441    linker_projection: TransformBundleLinkerProjectionV0,
442    emission_item_projection: TransformBundleEmissionItemProjectionV0,
443}
444
445impl TransformBundleLinkProjectionSetV0 {
446    pub fn linker_projection(&self) -> &TransformBundleLinkerProjectionV0 {
447        &self.linker_projection
448    }
449
450    pub fn emission_item_projection(&self) -> &TransformBundleEmissionItemProjectionV0 {
451        &self.emission_item_projection
452    }
453}
454
455impl TransformBundleLinkerProjectionV0 {
456    pub fn inputs(&self) -> &[LinkerInputV0] {
457        &self.inputs
458    }
459
460    pub fn module_reachability_evidence(
461        &self,
462        module_instance: &ModuleInstanceKeyV0,
463    ) -> ClosedWorldModuleReachabilityEvidenceV0 {
464        self.module_reachability_evidence
465            .get(module_instance)
466            .copied()
467            .unwrap_or_default()
468    }
469
470    pub fn module_reachability_derivation(
471        &self,
472        module_instance: &ModuleInstanceKeyV0,
473    ) -> Option<InstanceReachabilityDerivationV0> {
474        self.module_reachability_derivations
475            .get(module_instance)
476            .copied()
477    }
478}
479
480#[non_exhaustive]
481#[derive(Debug, Clone, PartialEq, Eq, Serialize)]
482#[serde(rename_all = "camelCase")]
483pub struct TransformBundleDependencyResolutionV0 {
484    pub attempt_state: &'static str,
485    pub policy_step_keys: Vec<&'static str>,
486    pub resolution_kind: Option<&'static str>,
487    pub candidate_count: usize,
488    pub target_instance: Option<ModuleInstanceKeyV0>,
489}
490
491impl TransformBundleDependencyResolutionV0 {
492    pub fn attempted(
493        policy_step_keys: Vec<&'static str>,
494        resolution_kind: &'static str,
495        candidate_count: usize,
496        target_instance: Option<ModuleInstanceKeyV0>,
497    ) -> Self {
498        Self {
499            attempt_state: "attempted",
500            policy_step_keys,
501            resolution_kind: Some(resolution_kind),
502            candidate_count,
503            target_instance,
504        }
505    }
506
507    pub fn never_attempted(policy_step_keys: Vec<&'static str>) -> Self {
508        Self {
509            attempt_state: "never-attempted",
510            policy_step_keys,
511            resolution_kind: None,
512            candidate_count: 0,
513            target_instance: None,
514        }
515    }
516}
517
518#[non_exhaustive]
519#[derive(Debug, Clone, PartialEq, Eq, Serialize)]
520#[serde(rename_all = "camelCase")]
521pub struct TransformBundleResolvedDependencyV0 {
522    pub source_instance: ModuleInstanceKeyV0,
523    pub edge_kind: TransformBundleEdgeKind,
524    pub import_source: String,
525    pub import_ordinal: Option<u32>,
526    pub resolution: TransformBundleDependencyResolutionV0,
527}
528
529impl TransformBundleResolvedDependencyV0 {
530    pub fn new(
531        source_instance: ModuleInstanceKeyV0,
532        edge_kind: TransformBundleEdgeKind,
533        import_source: impl Into<String>,
534        import_ordinal: Option<u32>,
535        resolution: TransformBundleDependencyResolutionV0,
536    ) -> Self {
537        Self {
538            source_instance,
539            edge_kind,
540            import_source: import_source.into(),
541            import_ordinal,
542            resolution,
543        }
544    }
545}
546
547#[non_exhaustive]
548#[derive(Debug, Clone, Copy, Default, PartialEq, Eq, PartialOrd, Ord, Serialize)]
549#[serde(rename_all = "camelCase")]
550pub enum BundleResolutionAuthorityV0 {
551    /// Every dependency edge must have a supplied resolved record.
552    Resolved,
553    /// Unmatched edges fall back to importer-relative path candidates.
554    #[default]
555    LegacyPathInferred,
556}
557
558#[non_exhaustive]
559#[derive(Debug, Clone, PartialEq, Eq, PartialOrd, Ord, Serialize)]
560#[serde(rename_all = "camelCase")]
561pub struct BundleDependencyResolutionDisclosureV0 {
562    pub source_instance: ModuleInstanceKeyV0,
563    pub import_source: String,
564    pub import_ordinal: Option<u32>,
565    pub authority: BundleResolutionAuthorityV0,
566}
567
568#[non_exhaustive]
569#[derive(Debug, Clone, Copy, Default, PartialEq, Eq, Serialize)]
570#[serde(rename_all = "camelCase")]
571pub struct TransformBundleLinkOptionsV0 {
572    pub emission_ordering_policy: EmissionOrderingPolicyV0,
573    pub dependency_resolution_authority: BundleResolutionAuthorityV0,
574}
575
576impl TransformBundleLinkOptionsV0 {
577    pub const fn with_emission_ordering_policy(
578        mut self,
579        emission_ordering_policy: EmissionOrderingPolicyV0,
580    ) -> Self {
581        self.emission_ordering_policy = emission_ordering_policy;
582        self
583    }
584
585    pub const fn with_dependency_resolution_authority(
586        mut self,
587        dependency_resolution_authority: BundleResolutionAuthorityV0,
588    ) -> Self {
589        self.dependency_resolution_authority = dependency_resolution_authority;
590        self
591    }
592}
593
594#[derive(Debug, Clone, PartialEq, Eq, Serialize)]
595#[serde(rename_all = "camelCase")]
596pub struct LinkedStylesheetRuleV0 {
597    pub global_order_index: u32,
598    pub module_instance: ModuleInstanceKeyV0,
599    pub selector_name: String,
600    pub selector_kind: &'static str,
601    pub range_start: u32,
602    pub range_end: u32,
603}
604
605impl LinkedStylesheetRuleV0 {
606    pub fn cascade_key_with_global_source_order(
607        &self,
608        level: CascadeLevel,
609        layer_ordinal: LayerOrdinal,
610        important: bool,
611        scope_proximity: u32,
612        specificity: Specificity,
613        module_rank: ModuleRank,
614    ) -> (CascadeKey, OpenWorldTieEvidence) {
615        (
616            CascadeKey::new(
617                level,
618                normalized_layer_rank(important, Some(layer_ordinal)),
619                scope_proximity,
620                specificity,
621                self.global_order_index,
622            ),
623            OpenWorldTieEvidence::new(module_rank),
624        )
625    }
626}
627
628#[derive(Debug, Clone, PartialEq, Eq, Serialize)]
629#[serde(rename_all = "camelCase")]
630pub struct GlobalRuleOrderV0 {
631    pub rules: Vec<LinkedStylesheetRuleV0>,
632}
633
634#[derive(Debug, Clone, PartialEq, Eq, Serialize)]
635#[serde(rename_all = "camelCase")]
636pub struct LinkedStylesheetV0 {
637    pub schema_version: &'static str,
638    pub product: &'static str,
639    pub entrypoints: Vec<ModuleInstanceKeyV0>,
640    pub module_instances: Vec<ModuleInstanceKeyV0>,
641    #[serde(skip_serializing)]
642    pub emission_plan: EmissionPlanV0,
643    pub global_rule_order: GlobalRuleOrderV0,
644    pub closed_world_bundle: ClosedWorldBundleV0,
645}
646
647#[derive(Debug, Clone, PartialEq, Eq, Serialize)]
648#[serde(rename_all = "camelCase")]
649#[non_exhaustive]
650pub struct LinkedStylesheetWithEmissionItemsV0 {
651    pub linked_stylesheet: LinkedStylesheetV0,
652    pub emission_item_plan: EmissionItemPlanV0,
653    pub emission_item_order: LinkedEmissionItemOrderV0,
654    pub projection_disclosures: Vec<EmissionItemProjectionDisclosureV0>,
655    pub dependency_resolution_disclosures: Vec<BundleDependencyResolutionDisclosureV0>,
656}
657
658/// Couples legacy admission evidence with a requested emission-policy result from one prepared graph.
659#[non_exhaustive]
660#[derive(Debug, Clone, PartialEq, Eq)]
661pub struct TransformBundleEmissionAdmissionV0 {
662    module_id_legacy_open: bool,
663    requested_policy_result:
664        Result<LinkedStylesheetWithEmissionItemsV0, TransformBundleLinkErrorV0>,
665}
666
667impl TransformBundleEmissionAdmissionV0 {
668    /// Reports whether legacy module-id ordering could not produce a closed linked stylesheet.
669    pub const fn module_id_legacy_open(&self) -> bool {
670        self.module_id_legacy_open
671    }
672
673    /// Borrows the requested emission-policy result.
674    pub fn requested_policy_result(
675        &self,
676    ) -> &Result<LinkedStylesheetWithEmissionItemsV0, TransformBundleLinkErrorV0> {
677        &self.requested_policy_result
678    }
679
680    /// Consumes the admission evidence and returns the requested emission-policy result.
681    pub fn into_requested_policy_result(
682        self,
683    ) -> Result<LinkedStylesheetWithEmissionItemsV0, TransformBundleLinkErrorV0> {
684        self.requested_policy_result
685    }
686
687    /// Consumes the evidence into the legacy admission decision and requested result.
688    pub fn into_parts(
689        self,
690    ) -> (
691        bool,
692        Result<LinkedStylesheetWithEmissionItemsV0, TransformBundleLinkErrorV0>,
693    ) {
694        (self.module_id_legacy_open, self.requested_policy_result)
695    }
696}
697
698#[derive(Debug, Clone, PartialEq, Eq, Serialize)]
699#[serde(rename_all = "camelCase")]
700pub struct TransformBundleTransformedModuleV0 {
701    pub module_instance: ModuleInstanceKeyV0,
702    pub output_css: String,
703    pub non_empty_import_replacement_count: usize,
704}
705
706impl TransformBundleTransformedModuleV0 {
707    pub fn new(module_instance: ModuleInstanceKeyV0, output_css: impl Into<String>) -> Self {
708        Self {
709            module_instance,
710            output_css: output_css.into(),
711            non_empty_import_replacement_count: 0,
712        }
713    }
714
715    pub const fn with_non_empty_import_replacement_count(mut self, count: usize) -> Self {
716        self.non_empty_import_replacement_count = count;
717        self
718    }
719}
720
721#[derive(Debug, Clone, PartialEq, Eq, Serialize)]
722#[serde(rename_all = "camelCase")]
723pub struct LinkedEmissionModuleRegionV0 {
724    pub module_instance: ModuleInstanceKeyV0,
725    pub first_global_order_index: Option<u32>,
726    pub generated_start: usize,
727    pub generated_end: usize,
728}
729
730#[derive(Debug, Clone, PartialEq, Eq, Serialize)]
731#[serde(rename_all = "camelCase")]
732pub struct LinkedEmissionOrderEntryRegionV0 {
733    pub global_order_index: u32,
734    pub module_instance: ModuleInstanceKeyV0,
735    pub generated_start: usize,
736    pub generated_end: usize,
737}
738
739#[derive(Debug, Clone, PartialEq, Eq, Serialize)]
740#[serde(rename_all = "camelCase")]
741pub struct LinkedEmissionArtifactV0 {
742    pub schema_version: &'static str,
743    pub product: &'static str,
744    pub output_css: String,
745    pub module_regions: Vec<LinkedEmissionModuleRegionV0>,
746    pub order_entry_regions: Vec<LinkedEmissionOrderEntryRegionV0>,
747    pub emitted_module_count: usize,
748    pub global_order_entry_count: usize,
749}
750
751#[derive(Debug, Clone, PartialEq, Eq, Serialize)]
752#[serde(rename_all = "camelCase")]
753pub enum LinkedEmissionMaterializationErrorV0 {
754    DuplicateTransformedModule {
755        module_instance: ModuleInstanceKeyV0,
756    },
757    MissingTransformedModule {
758        module_instance: ModuleInstanceKeyV0,
759    },
760    UnexpectedTransformedModule {
761        module_instance: ModuleInstanceKeyV0,
762    },
763    ImportReplacementWouldDuplicateModule {
764        module_instance: ModuleInstanceKeyV0,
765        replacement_count: usize,
766    },
767    InvalidGlobalOrderIndex {
768        expected: u32,
769        actual: u32,
770    },
771}
772
773#[derive(Debug, Clone, PartialEq, Eq, Serialize)]
774#[serde(rename_all = "camelCase")]
775pub struct EmissionPolicyDifferenceV0 {
776    pub output_index: u32,
777    pub module_id_legacy_module: Option<ModuleInstanceKeyV0>,
778    pub module_id_legacy_selector: Option<String>,
779    pub import_order_module: Option<ModuleInstanceKeyV0>,
780    pub import_order_selector: Option<String>,
781}
782
783#[derive(Debug, Clone, PartialEq, Eq, Serialize)]
784#[serde(rename_all = "camelCase")]
785pub struct EmissionPolicyDifferentialReportV0 {
786    pub schema_version: &'static str,
787    pub product: &'static str,
788    pub module_id_legacy_rule_count: usize,
789    pub import_order_rule_count: usize,
790    pub difference_count: usize,
791    pub equivalent: bool,
792    pub differences: Vec<EmissionPolicyDifferenceV0>,
793}
794
795#[derive(Debug, Clone, PartialEq, Eq, Serialize)]
796#[serde(rename_all = "camelCase")]
797pub enum TransformBundleLinkErrorV0 {
798    MissingEntrypoint {
799        source_path: String,
800    },
801    AmbiguousModulePath {
802        source_path: String,
803    },
804    MissingDependency {
805        source_path: String,
806        import_source: String,
807    },
808    UnresolvedDependencyEdge {
809        source_path: String,
810        import_source: String,
811        import_ordinal: Option<u32>,
812    },
813    ClosedWorldBundle {
814        error: ClosedWorldBundleBuildErrorV0,
815    },
816    InvalidEmissionPlan {
817        reason: String,
818    },
819    UnsupportedEmissionCycle {
820        edge_kind: TransformBundleEdgeKind,
821    },
822}
823
824pub fn summarize_omena_transform_bundle_from_source(
825    source_path: impl Into<String>,
826    source: &str,
827    dialect: StyleDialect,
828) -> TransformBundleSourceSummaryV0 {
829    let source_path = source_path.into();
830    let facts = collect_style_facts(source, dialect);
831    let bundle_edges = collect_bundle_edges_from_facts(&source_path, dialect, &facts);
832    let asset_urls = collect_transform_ir_bundle_asset_urls(&source_path, source, dialect);
833    let code_split_chunks = plan_bundle_code_split_chunks(&source_path, &bundle_edges, &asset_urls);
834    let mut required_passes =
835        required_passes_for_source(&source_path, dialect, &facts, &bundle_edges);
836    required_passes.sort_by_key(|pass| transform_pass_sort_ordinal(*pass));
837    required_passes.dedup();
838    let pass_plan = plan_transform_passes(&required_passes);
839    let planned_pass_ids = pass_plan.ordered_pass_ids.clone();
840    let required_pass_ids = required_passes
841        .iter()
842        .map(|pass| pass.id())
843        .collect::<Vec<_>>();
844
845    TransformBundleSourceSummaryV0 {
846        schema_version: "0",
847        product: "omena-transform-bundle.source",
848        source_path,
849        dialect: dialect_label(dialect),
850        bundle_edges,
851        asset_urls,
852        code_splitting_required: code_split_chunks.len() > 1,
853        code_split_chunks,
854        required_pass_ids,
855        planned_pass_ids,
856        import_inline_required: required_passes.contains(&TransformPassKind::ImportInline),
857        module_evaluation_required: required_passes.iter().any(|pass| {
858            matches!(
859                pass,
860                TransformPassKind::ScssModuleEvaluate | TransformPassKind::LessModuleEvaluate
861            )
862        }),
863        css_modules_resolution_required: required_passes.iter().any(|pass| {
864            matches!(
865                pass,
866                TransformPassKind::HashCssModuleClassNames
867                    | TransformPassKind::ResolveCssModulesComposes
868            )
869        }),
870        class_hashing_required: required_passes
871            .contains(&TransformPassKind::HashCssModuleClassNames),
872        value_resolution_required: required_passes.contains(&TransformPassKind::ValueResolution),
873        pass_plan,
874    }
875}
876
877/// Legacy LinkedStylesheetV0 entry points do not expose dependency resolution provenance.
878pub fn link_omena_transform_bundle_modules<P: AsRef<str>>(
879    entrypoint_paths: &[P],
880    modules: &[TransformBundleModuleInputV0],
881) -> Result<LinkedStylesheetV0, TransformBundleLinkErrorV0> {
882    link_omena_transform_bundle_modules_with_semantic_reachability(entrypoint_paths, modules, &[])
883}
884
885/// Legacy LinkedStylesheetV0 entry points do not expose dependency resolution provenance.
886#[allow(deprecated)]
887pub fn link_omena_transform_bundle_modules_with_semantic_reachability<P: AsRef<str>>(
888    entrypoint_paths: &[P],
889    modules: &[TransformBundleModuleInputV0],
890    reachability_inputs: &[TransformBundleSemanticReachabilityInputV0],
891) -> Result<LinkedStylesheetV0, TransformBundleLinkErrorV0> {
892    link_omena_transform_bundle_modules_with_semantic_reachability_and_metadata(
893        entrypoint_paths,
894        modules,
895        reachability_inputs,
896        &[],
897    )
898}
899
900/// Legacy LinkedStylesheetV0 entry points do not expose dependency resolution provenance.
901#[allow(deprecated)]
902pub fn link_omena_transform_bundle_modules_with_semantic_reachability_and_metadata<
903    P: AsRef<str>,
904>(
905    entrypoint_paths: &[P],
906    modules: &[TransformBundleModuleInputV0],
907    reachability_inputs: &[TransformBundleSemanticReachabilityInputV0],
908    module_metadata: &[ClosedWorldModuleMetadataV0],
909) -> Result<LinkedStylesheetV0, TransformBundleLinkErrorV0> {
910    link_omena_transform_bundle_modules_with_options(
911        entrypoint_paths,
912        modules,
913        reachability_inputs,
914        module_metadata,
915        TransformBundleLinkOptionsV0::default(),
916    )
917}
918
919/// Legacy LinkedStylesheetV0 entry points do not expose dependency resolution provenance.
920#[allow(deprecated)]
921pub fn link_omena_transform_bundle_modules_with_options<P: AsRef<str>>(
922    entrypoint_paths: &[P],
923    modules: &[TransformBundleModuleInputV0],
924    reachability_inputs: &[TransformBundleSemanticReachabilityInputV0],
925    module_metadata: &[ClosedWorldModuleMetadataV0],
926    options: TransformBundleLinkOptionsV0,
927) -> Result<LinkedStylesheetV0, TransformBundleLinkErrorV0> {
928    let projection = project_omena_transform_bundle_linker_inputs(modules, reachability_inputs);
929    link_omena_transform_bundle_projection_with_resolved_dependencies_and_options(
930        entrypoint_paths,
931        &projection,
932        &[],
933        module_metadata,
934        options,
935    )
936}
937
938#[allow(deprecated)]
939pub fn project_omena_transform_bundle_linker_inputs(
940    modules: &[TransformBundleModuleInputV0],
941    reachability_inputs: &[TransformBundleSemanticReachabilityInputV0],
942) -> TransformBundleLinkerProjectionV0 {
943    let parsed_modules = modules
944        .iter()
945        .map(|module| {
946            TransformBundleParsedModuleInputV0::new(
947                module.source_path.as_str(),
948                module.dialect,
949                collect_style_facts(module.source.as_str(), module.dialect),
950            )
951            .with_configuration_hashes(vec![module.configuration_hash.clone()])
952        })
953        .collect::<Vec<_>>();
954    project_omena_transform_bundle_linker_inputs_from_parsed_modules(
955        parsed_modules.as_slice(),
956        reachability_inputs,
957    )
958}
959
960#[allow(deprecated)]
961pub fn project_omena_transform_bundle_linker_and_emission_items(
962    modules: &[TransformBundleModuleInputV0],
963    reachability_inputs: &[TransformBundleSemanticReachabilityInputV0],
964) -> TransformBundleLinkProjectionSetV0 {
965    let parsed_modules = modules
966        .iter()
967        .map(|module| {
968            let collection = collect_style_fact_collection(module.source.as_str(), module.dialect);
969            TransformBundleParsedModuleInputV0::new(
970                module.source_path.as_str(),
971                module.dialect,
972                collection.facts,
973            )
974            .with_emission_selectors(collection.emission_selectors)
975            .with_configuration_hashes(vec![module.configuration_hash.clone()])
976        })
977        .collect::<Vec<_>>();
978    project_omena_transform_bundle_linker_and_emission_items_from_parsed_modules(
979        parsed_modules.as_slice(),
980        reachability_inputs,
981    )
982}
983
984#[allow(deprecated)]
985pub fn project_omena_transform_bundle_linker_inputs_from_parsed_modules(
986    modules: &[TransformBundleParsedModuleInputV0],
987    reachability_inputs: &[TransformBundleSemanticReachabilityInputV0],
988) -> TransformBundleLinkerProjectionV0 {
989    let instance_reachability_inputs =
990        fan_out_path_reachability_to_instances(modules, reachability_inputs);
991    project_omena_transform_bundle_linker_inputs_from_parsed_modules_with_instance_reachability(
992        modules,
993        instance_reachability_inputs.as_slice(),
994    )
995}
996
997pub fn project_omena_transform_bundle_linker_inputs_from_parsed_modules_with_instance_reachability(
998    modules: &[TransformBundleParsedModuleInputV0],
999    reachability_inputs: &[TransformBundleInstanceReachabilityInputV0],
1000) -> TransformBundleLinkerProjectionV0 {
1001    let mut inputs = Vec::new();
1002    for module in modules {
1003        let source_path = normalize_bundle_path(PathBuf::from(module.source_path.as_str()));
1004        let bundle_edges =
1005            collect_bundle_edges_from_facts(&source_path, module.dialect, &module.facts);
1006        for instance in module.module_instance_keys() {
1007            inputs.push(linker_input_from_module_facts(
1008                source_path.as_str(),
1009                instance,
1010                &module.facts,
1011                bundle_edges.as_slice(),
1012            ));
1013        }
1014    }
1015    let (module_reachability_evidence, module_reachability_derivations) =
1016        apply_semantic_reachability_to_linker_inputs(inputs.as_mut_slice(), reachability_inputs);
1017    TransformBundleLinkerProjectionV0 {
1018        inputs,
1019        module_reachability_evidence,
1020        module_reachability_derivations,
1021    }
1022}
1023
1024#[allow(deprecated)]
1025pub fn project_omena_transform_bundle_linker_and_emission_items_from_parsed_modules(
1026    modules: &[TransformBundleParsedModuleInputV0],
1027    reachability_inputs: &[TransformBundleSemanticReachabilityInputV0],
1028) -> TransformBundleLinkProjectionSetV0 {
1029    let instance_reachability_inputs =
1030        fan_out_path_reachability_to_instances(modules, reachability_inputs);
1031    project_omena_transform_bundle_linker_and_emission_items_from_parsed_modules_with_instance_reachability(
1032        modules,
1033        instance_reachability_inputs.as_slice(),
1034    )
1035}
1036
1037pub fn project_omena_transform_bundle_linker_and_emission_items_from_parsed_modules_with_instance_reachability(
1038    modules: &[TransformBundleParsedModuleInputV0],
1039    reachability_inputs: &[TransformBundleInstanceReachabilityInputV0],
1040) -> TransformBundleLinkProjectionSetV0 {
1041    let linker_projection =
1042        project_omena_transform_bundle_linker_inputs_from_parsed_modules_with_instance_reachability(
1043            modules,
1044            reachability_inputs,
1045        );
1046    let mut emission_item_inputs = Vec::new();
1047    for module in modules {
1048        let items =
1049            emission_items::collect_emission_items(&module.facts, &module.emission_selectors);
1050        let disclosure = emission_items::emission_item_projection_disclosure(&module.facts);
1051        for instance in module.module_instance_keys() {
1052            emission_item_inputs.push(EmissionItemInputV0 {
1053                module_instance: instance,
1054                items: items.clone(),
1055                disclosure: disclosure.clone(),
1056            });
1057        }
1058    }
1059    TransformBundleLinkProjectionSetV0 {
1060        linker_projection,
1061        emission_item_projection: TransformBundleEmissionItemProjectionV0::new(
1062            emission_item_inputs,
1063        ),
1064    }
1065}
1066
1067/// Legacy LinkedStylesheetV0 entry points do not expose dependency resolution provenance.
1068pub fn link_omena_transform_bundle_projection_with_resolved_dependencies_and_options<
1069    P: AsRef<str>,
1070>(
1071    entrypoint_paths: &[P],
1072    projection: &TransformBundleLinkerProjectionV0,
1073    resolved_dependencies: &[TransformBundleResolvedDependencyV0],
1074    module_metadata: &[ClosedWorldModuleMetadataV0],
1075    options: TransformBundleLinkOptionsV0,
1076) -> Result<LinkedStylesheetV0, TransformBundleLinkErrorV0> {
1077    let entrypoint_paths = entrypoint_paths
1078        .iter()
1079        .map(|path| path.as_ref())
1080        .collect::<Vec<_>>();
1081
1082    link_stylesheet_from_projection_with_metadata_and_options(
1083        entrypoint_paths.as_slice(),
1084        projection.inputs(),
1085        resolved_dependencies,
1086        module_metadata,
1087        &projection.module_reachability_evidence,
1088        options,
1089    )
1090}
1091
1092pub fn link_omena_transform_bundle_projection_with_emission_items_and_resolved_dependencies_and_options<
1093    P: AsRef<str>,
1094>(
1095    entrypoint_paths: &[P],
1096    linker_projection: &TransformBundleLinkerProjectionV0,
1097    emission_item_projection: &TransformBundleEmissionItemProjectionV0,
1098    resolved_dependencies: &[TransformBundleResolvedDependencyV0],
1099    module_metadata: &[ClosedWorldModuleMetadataV0],
1100    options: TransformBundleLinkOptionsV0,
1101) -> Result<LinkedStylesheetWithEmissionItemsV0, TransformBundleLinkErrorV0> {
1102    let entrypoint_paths = entrypoint_paths
1103        .iter()
1104        .map(|path| path.as_ref())
1105        .collect::<Vec<_>>();
1106
1107    link_stylesheet_from_projection_with_emission_items_and_metadata_and_options(
1108        entrypoint_paths.as_slice(),
1109        linker_projection.inputs(),
1110        emission_item_projection.inputs(),
1111        resolved_dependencies,
1112        module_metadata,
1113        &linker_projection.module_reachability_evidence,
1114        options,
1115    )
1116}
1117
1118pub fn link_resolved_bundle<P: AsRef<str>>(
1119    entrypoint_paths: &[P],
1120    linker_projection: &TransformBundleLinkerProjectionV0,
1121    emission_item_projection: &TransformBundleEmissionItemProjectionV0,
1122    resolved_dependencies: &[TransformBundleResolvedDependencyV0],
1123    module_metadata: &[ClosedWorldModuleMetadataV0],
1124    emission_ordering_policy: EmissionOrderingPolicyV0,
1125) -> Result<LinkedStylesheetWithEmissionItemsV0, TransformBundleLinkErrorV0> {
1126    link_omena_transform_bundle_projection_with_emission_items_and_resolved_dependencies_and_options(
1127        entrypoint_paths,
1128        linker_projection,
1129        emission_item_projection,
1130        resolved_dependencies,
1131        module_metadata,
1132        TransformBundleLinkOptionsV0::default()
1133            .with_emission_ordering_policy(emission_ordering_policy)
1134            .with_dependency_resolution_authority(BundleResolutionAuthorityV0::Resolved),
1135    )
1136}
1137
1138#[deprecated(
1139    note = "supply resolved dependencies and use link_resolved_bundle when dependency authority must be complete"
1140)]
1141pub fn link_legacy_path_inferred_bundle<P: AsRef<str>>(
1142    entrypoint_paths: &[P],
1143    linker_projection: &TransformBundleLinkerProjectionV0,
1144    emission_item_projection: &TransformBundleEmissionItemProjectionV0,
1145    resolved_dependencies: &[TransformBundleResolvedDependencyV0],
1146    module_metadata: &[ClosedWorldModuleMetadataV0],
1147    emission_ordering_policy: EmissionOrderingPolicyV0,
1148) -> Result<LinkedStylesheetWithEmissionItemsV0, TransformBundleLinkErrorV0> {
1149    link_omena_transform_bundle_projection_with_emission_items_and_resolved_dependencies_and_options(
1150        entrypoint_paths,
1151        linker_projection,
1152        emission_item_projection,
1153        resolved_dependencies,
1154        module_metadata,
1155        TransformBundleLinkOptionsV0::default()
1156            .with_emission_ordering_policy(emission_ordering_policy)
1157            .with_dependency_resolution_authority(BundleResolutionAuthorityV0::LegacyPathInferred),
1158    )
1159}
1160
1161/// Evaluates legacy admission and the requested emission policy from one closed-world preparation.
1162pub fn evaluate_omena_transform_bundle_projection_emission_admission_with_resolved_dependencies_and_options<
1163    P: AsRef<str>,
1164>(
1165    entrypoint_paths: &[P],
1166    linker_projection: &TransformBundleLinkerProjectionV0,
1167    emission_item_projection: &TransformBundleEmissionItemProjectionV0,
1168    resolved_dependencies: &[TransformBundleResolvedDependencyV0],
1169    module_metadata: &[ClosedWorldModuleMetadataV0],
1170    options: TransformBundleLinkOptionsV0,
1171) -> TransformBundleEmissionAdmissionV0 {
1172    let entrypoint_paths = entrypoint_paths
1173        .iter()
1174        .map(|path| path.as_ref())
1175        .collect::<Vec<_>>();
1176
1177    evaluate_stylesheet_emission_admission_from_projection(
1178        entrypoint_paths.as_slice(),
1179        linker_projection.inputs(),
1180        emission_item_projection.inputs(),
1181        resolved_dependencies,
1182        module_metadata,
1183        &linker_projection.module_reachability_evidence,
1184        options,
1185    )
1186}
1187
1188pub fn compare_omena_transform_bundle_emission_policies<P: AsRef<str>>(
1189    entrypoint_paths: &[P],
1190    modules: &[TransformBundleModuleInputV0],
1191) -> Result<EmissionPolicyDifferentialReportV0, TransformBundleLinkErrorV0> {
1192    let module_id_legacy = link_omena_transform_bundle_modules_with_options(
1193        entrypoint_paths,
1194        modules,
1195        &[],
1196        &[],
1197        TransformBundleLinkOptionsV0 {
1198            emission_ordering_policy: EmissionOrderingPolicyV0::ModuleIdLegacy,
1199            ..TransformBundleLinkOptionsV0::default()
1200        },
1201    )?;
1202    let import_order = link_omena_transform_bundle_modules_with_options(
1203        entrypoint_paths,
1204        modules,
1205        &[],
1206        &[],
1207        TransformBundleLinkOptionsV0 {
1208            emission_ordering_policy: EmissionOrderingPolicyV0::ImportOrderPreserving,
1209            ..TransformBundleLinkOptionsV0::default()
1210        },
1211    )?;
1212    let module_id_legacy_rules = &module_id_legacy.global_rule_order.rules;
1213    let import_order_rules = &import_order.global_rule_order.rules;
1214    let mut differences = Vec::new();
1215    for output_index in 0..module_id_legacy_rules.len().max(import_order_rules.len()) {
1216        let module_id_legacy_rule = module_id_legacy_rules.get(output_index);
1217        let import_order_rule = import_order_rules.get(output_index);
1218        if module_id_legacy_rule == import_order_rule {
1219            continue;
1220        }
1221        differences.push(EmissionPolicyDifferenceV0 {
1222            output_index: u32::try_from(output_index).map_err(|_| {
1223                TransformBundleLinkErrorV0::InvalidEmissionPlan {
1224                    reason: "policy differential has more rows than the output index can represent"
1225                        .to_string(),
1226                }
1227            })?,
1228            module_id_legacy_module: module_id_legacy_rule.map(|rule| rule.module_instance.clone()),
1229            module_id_legacy_selector: module_id_legacy_rule.map(|rule| rule.selector_name.clone()),
1230            import_order_module: import_order_rule.map(|rule| rule.module_instance.clone()),
1231            import_order_selector: import_order_rule.map(|rule| rule.selector_name.clone()),
1232        });
1233    }
1234    let difference_count = differences.len();
1235    Ok(EmissionPolicyDifferentialReportV0 {
1236        schema_version: "0",
1237        product: "omena-bundler.emission-policy-differential",
1238        module_id_legacy_rule_count: module_id_legacy_rules.len(),
1239        import_order_rule_count: import_order_rules.len(),
1240        difference_count,
1241        equivalent: difference_count == 0,
1242        differences,
1243    })
1244}
1245
1246pub fn materialize_omena_transform_bundle_linked_stylesheet(
1247    linked: &LinkedStylesheetV0,
1248    transformed_modules: &[TransformBundleTransformedModuleV0],
1249) -> Result<LinkedEmissionArtifactV0, LinkedEmissionMaterializationErrorV0> {
1250    let (module_order, first_order_index_by_instance) =
1251        legacy_materialization_module_order(linked)?;
1252    materialize_linked_stylesheet_in_module_order(
1253        linked,
1254        transformed_modules,
1255        module_order,
1256        &first_order_index_by_instance,
1257    )
1258}
1259
1260pub fn materialize_omena_transform_bundle_linked_stylesheet_with_emission_items(
1261    linked: &LinkedStylesheetWithEmissionItemsV0,
1262    transformed_modules: &[TransformBundleTransformedModuleV0],
1263) -> Result<LinkedEmissionArtifactV0, LinkedEmissionItemMaterializationErrorV0> {
1264    let linked_modules = linked
1265        .linked_stylesheet
1266        .module_instances
1267        .iter()
1268        .cloned()
1269        .collect::<BTreeSet<_>>();
1270    let mut represented_modules = BTreeSet::new();
1271    let mut module_order = Vec::new();
1272    for (expected_index, item) in linked.emission_item_order.items.iter().enumerate() {
1273        let expected_index = u32::try_from(expected_index).unwrap_or(u32::MAX);
1274        if item.global_order_index != expected_index {
1275            return Err(
1276                LinkedEmissionItemMaterializationErrorV0::InvalidItemOrderIndex {
1277                    expected: expected_index,
1278                    actual: item.global_order_index,
1279                },
1280            );
1281        }
1282        if !linked_modules.contains(&item.module_instance) {
1283            return Err(
1284                LinkedEmissionItemMaterializationErrorV0::UnknownEmissionItemModule {
1285                    module_instance: item.module_instance.clone(),
1286                },
1287            );
1288        }
1289        if represented_modules.insert(item.module_instance.clone()) {
1290            module_order.push(item.module_instance.clone());
1291        }
1292    }
1293    for module_instance in &linked.linked_stylesheet.module_instances {
1294        if !represented_modules.contains(module_instance) {
1295            return Err(
1296                LinkedEmissionItemMaterializationErrorV0::MissingEmissionItem {
1297                    module_instance: module_instance.clone(),
1298                },
1299            );
1300        }
1301    }
1302
1303    let (_, first_order_index_by_instance) =
1304        selector_materialization_module_order(&linked.linked_stylesheet)?;
1305    materialize_linked_stylesheet_in_module_order(
1306        &linked.linked_stylesheet,
1307        transformed_modules,
1308        module_order,
1309        &first_order_index_by_instance,
1310    )
1311    .map_err(LinkedEmissionItemMaterializationErrorV0::from)
1312}
1313
1314fn legacy_materialization_module_order(
1315    linked: &LinkedStylesheetV0,
1316) -> Result<
1317    (Vec<ModuleInstanceKeyV0>, BTreeMap<ModuleInstanceKeyV0, u32>),
1318    LinkedEmissionMaterializationErrorV0,
1319> {
1320    let (mut module_order, first_order_index_by_instance) =
1321        selector_materialization_module_order(linked)?;
1322    for module_instance in &linked.module_instances {
1323        if !first_order_index_by_instance.contains_key(module_instance) {
1324            module_order.push(module_instance.clone());
1325        }
1326    }
1327    Ok((module_order, first_order_index_by_instance))
1328}
1329
1330fn selector_materialization_module_order(
1331    linked: &LinkedStylesheetV0,
1332) -> Result<
1333    (Vec<ModuleInstanceKeyV0>, BTreeMap<ModuleInstanceKeyV0, u32>),
1334    LinkedEmissionMaterializationErrorV0,
1335> {
1336    let mut first_order_index_by_instance = BTreeMap::new();
1337    let mut module_order = Vec::new();
1338    for (expected_index, rule) in linked.global_rule_order.rules.iter().enumerate() {
1339        let expected_index = u32::try_from(expected_index).unwrap_or(u32::MAX);
1340        if rule.global_order_index != expected_index {
1341            return Err(
1342                LinkedEmissionMaterializationErrorV0::InvalidGlobalOrderIndex {
1343                    expected: expected_index,
1344                    actual: rule.global_order_index,
1345                },
1346            );
1347        }
1348        if first_order_index_by_instance
1349            .insert(rule.module_instance.clone(), rule.global_order_index)
1350            .is_none()
1351        {
1352            module_order.push(rule.module_instance.clone());
1353        }
1354    }
1355    Ok((module_order, first_order_index_by_instance))
1356}
1357
1358fn materialize_linked_stylesheet_in_module_order(
1359    linked: &LinkedStylesheetV0,
1360    transformed_modules: &[TransformBundleTransformedModuleV0],
1361    module_order: Vec<ModuleInstanceKeyV0>,
1362    first_order_index_by_instance: &BTreeMap<ModuleInstanceKeyV0, u32>,
1363) -> Result<LinkedEmissionArtifactV0, LinkedEmissionMaterializationErrorV0> {
1364    let linked_modules = linked
1365        .module_instances
1366        .iter()
1367        .cloned()
1368        .collect::<BTreeSet<_>>();
1369    let mut transformed_by_instance = BTreeMap::new();
1370    for transformed in transformed_modules {
1371        if !linked_modules.contains(&transformed.module_instance) {
1372            return Err(
1373                LinkedEmissionMaterializationErrorV0::UnexpectedTransformedModule {
1374                    module_instance: transformed.module_instance.clone(),
1375                },
1376            );
1377        }
1378        if transformed.non_empty_import_replacement_count > 0 {
1379            return Err(
1380                LinkedEmissionMaterializationErrorV0::ImportReplacementWouldDuplicateModule {
1381                    module_instance: transformed.module_instance.clone(),
1382                    replacement_count: transformed.non_empty_import_replacement_count,
1383                },
1384            );
1385        }
1386        if transformed_by_instance
1387            .insert(transformed.module_instance.clone(), transformed)
1388            .is_some()
1389        {
1390            return Err(
1391                LinkedEmissionMaterializationErrorV0::DuplicateTransformedModule {
1392                    module_instance: transformed.module_instance.clone(),
1393                },
1394            );
1395        }
1396    }
1397
1398    for module_instance in &linked.module_instances {
1399        if !transformed_by_instance.contains_key(module_instance) {
1400            return Err(
1401                LinkedEmissionMaterializationErrorV0::MissingTransformedModule {
1402                    module_instance: module_instance.clone(),
1403                },
1404            );
1405        }
1406    }
1407
1408    let mut output_css = String::new();
1409    let mut module_regions = Vec::with_capacity(module_order.len());
1410    let mut generated_region_by_instance = BTreeMap::new();
1411    for module_instance in module_order {
1412        let Some(transformed) = transformed_by_instance.get(&module_instance) else {
1413            return Err(
1414                LinkedEmissionMaterializationErrorV0::MissingTransformedModule { module_instance },
1415            );
1416        };
1417        if !output_css.is_empty()
1418            && !output_css.ends_with('\n')
1419            && !transformed.output_css.is_empty()
1420        {
1421            output_css.push('\n');
1422        }
1423        let generated_start = output_css.len();
1424        output_css.push_str(&transformed.output_css);
1425        let generated_end = output_css.len();
1426        generated_region_by_instance
1427            .insert(module_instance.clone(), (generated_start, generated_end));
1428        module_regions.push(LinkedEmissionModuleRegionV0 {
1429            first_global_order_index: first_order_index_by_instance.get(&module_instance).copied(),
1430            module_instance,
1431            generated_start,
1432            generated_end,
1433        });
1434    }
1435
1436    let mut order_entry_regions = Vec::with_capacity(linked.global_rule_order.rules.len());
1437    for rule in &linked.global_rule_order.rules {
1438        let Some((generated_start, generated_end)) = generated_region_by_instance
1439            .get(&rule.module_instance)
1440            .copied()
1441        else {
1442            return Err(
1443                LinkedEmissionMaterializationErrorV0::MissingTransformedModule {
1444                    module_instance: rule.module_instance.clone(),
1445                },
1446            );
1447        };
1448        order_entry_regions.push(LinkedEmissionOrderEntryRegionV0 {
1449            global_order_index: rule.global_order_index,
1450            module_instance: rule.module_instance.clone(),
1451            generated_start,
1452            generated_end,
1453        });
1454    }
1455
1456    Ok(LinkedEmissionArtifactV0 {
1457        schema_version: "0",
1458        product: "omena-transform-bundle.linked-emission",
1459        emitted_module_count: module_regions.len(),
1460        global_order_entry_count: order_entry_regions.len(),
1461        output_css,
1462        module_regions,
1463        order_entry_regions,
1464    })
1465}
1466
1467/// Legacy LinkedStylesheetV0 entry points do not expose dependency resolution provenance.
1468pub fn link_stylesheet_from_projection(
1469    entrypoint_paths: &[&str],
1470    inputs: &[LinkerInputV0],
1471) -> Result<LinkedStylesheetV0, TransformBundleLinkErrorV0> {
1472    link_stylesheet_from_projection_with_options(
1473        entrypoint_paths,
1474        inputs,
1475        TransformBundleLinkOptionsV0::default(),
1476    )
1477}
1478
1479/// Legacy LinkedStylesheetV0 entry points do not expose dependency resolution provenance.
1480pub fn link_stylesheet_from_projection_with_options(
1481    entrypoint_paths: &[&str],
1482    inputs: &[LinkerInputV0],
1483    options: TransformBundleLinkOptionsV0,
1484) -> Result<LinkedStylesheetV0, TransformBundleLinkErrorV0> {
1485    link_stylesheet_from_projection_with_resolved_dependencies_and_options(
1486        entrypoint_paths,
1487        inputs,
1488        &[],
1489        options,
1490    )
1491}
1492
1493/// Legacy LinkedStylesheetV0 entry points do not expose dependency resolution provenance.
1494pub fn link_stylesheet_from_projection_with_resolved_dependencies_and_options(
1495    entrypoint_paths: &[&str],
1496    inputs: &[LinkerInputV0],
1497    resolved_dependencies: &[TransformBundleResolvedDependencyV0],
1498    options: TransformBundleLinkOptionsV0,
1499) -> Result<LinkedStylesheetV0, TransformBundleLinkErrorV0> {
1500    link_stylesheet_from_projection_with_metadata_and_options(
1501        entrypoint_paths,
1502        inputs,
1503        resolved_dependencies,
1504        &[],
1505        &BTreeMap::new(),
1506        options,
1507    )
1508}
1509
1510fn link_stylesheet_from_projection_with_metadata_and_options(
1511    entrypoint_paths: &[&str],
1512    inputs: &[LinkerInputV0],
1513    resolved_dependencies: &[TransformBundleResolvedDependencyV0],
1514    module_metadata: &[ClosedWorldModuleMetadataV0],
1515    module_reachability_evidence: &BTreeMap<
1516        ModuleInstanceKeyV0,
1517        ClosedWorldModuleReachabilityEvidenceV0,
1518    >,
1519    options: TransformBundleLinkOptionsV0,
1520) -> Result<LinkedStylesheetV0, TransformBundleLinkErrorV0> {
1521    let prepared = prepare_linked_stylesheet_context(
1522        entrypoint_paths,
1523        inputs,
1524        resolved_dependencies,
1525        module_metadata,
1526        module_reachability_evidence,
1527        options.dependency_resolution_authority,
1528    )?;
1529    link_stylesheet_from_prepared_context(prepared, inputs, resolved_dependencies, options)
1530}
1531
1532fn link_stylesheet_from_prepared_context(
1533    prepared: PreparedLinkedStylesheetContextV0,
1534    inputs: &[LinkerInputV0],
1535    resolved_dependencies: &[TransformBundleResolvedDependencyV0],
1536    options: TransformBundleLinkOptionsV0,
1537) -> Result<LinkedStylesheetV0, TransformBundleLinkErrorV0> {
1538    let (emission_plan, global_rule_order) = build_linked_stylesheet_order_from_prepared_context(
1539        &prepared,
1540        inputs,
1541        resolved_dependencies,
1542        options,
1543    )?;
1544    Ok(LinkedStylesheetV0 {
1545        schema_version: "0",
1546        product: "omena-transform-bundle.linked-stylesheet",
1547        entrypoints: prepared.entrypoints,
1548        module_instances: prepared.closed_world_bundle.linked_modules().to_vec(),
1549        emission_plan,
1550        global_rule_order,
1551        closed_world_bundle: prepared.closed_world_bundle,
1552    })
1553}
1554
1555fn build_linked_stylesheet_order_from_prepared_context(
1556    prepared: &PreparedLinkedStylesheetContextV0,
1557    inputs: &[LinkerInputV0],
1558    resolved_dependencies: &[TransformBundleResolvedDependencyV0],
1559    options: TransformBundleLinkOptionsV0,
1560) -> Result<(EmissionPlanV0, GlobalRuleOrderV0), TransformBundleLinkErrorV0> {
1561    let emission_plan = emission_order::build_emission_plan(
1562        inputs,
1563        prepared.closed_world_bundle.linked_modules(),
1564        &prepared.entrypoints,
1565        resolved_dependencies,
1566        options.emission_ordering_policy,
1567        options.dependency_resolution_authority,
1568    )?;
1569    let global_rule_order =
1570        emission_order::build_global_rule_order_from_plan(inputs, &emission_plan)?;
1571    Ok((emission_plan, global_rule_order))
1572}
1573
1574struct PreparedLinkedStylesheetContextV0 {
1575    entrypoints: Vec<ModuleInstanceKeyV0>,
1576    closed_world_bundle: ClosedWorldBundleV0,
1577    dependency_resolution_disclosures: Vec<BundleDependencyResolutionDisclosureV0>,
1578}
1579
1580fn prepare_linked_stylesheet_context(
1581    entrypoint_paths: &[&str],
1582    inputs: &[LinkerInputV0],
1583    resolved_dependencies: &[TransformBundleResolvedDependencyV0],
1584    module_metadata: &[ClosedWorldModuleMetadataV0],
1585    module_reachability_evidence: &BTreeMap<
1586        ModuleInstanceKeyV0,
1587        ClosedWorldModuleReachabilityEvidenceV0,
1588    >,
1589    resolution_authority: BundleResolutionAuthorityV0,
1590) -> Result<PreparedLinkedStylesheetContextV0, TransformBundleLinkErrorV0> {
1591    let instances_by_path = module_instances_by_linker_path(inputs);
1592    let entrypoints = entrypoint_paths
1593        .iter()
1594        .map(|path| {
1595            resolve_entrypoint_module_instance_by_path(path, &instances_by_path)?.ok_or_else(|| {
1596                TransformBundleLinkErrorV0::MissingEntrypoint {
1597                    source_path: normalize_bundle_path(PathBuf::from(*path)),
1598                }
1599            })
1600        })
1601        .collect::<Result<Vec<_>, _>>()?;
1602    let (linked_modules, dependency_resolution_disclosures) =
1603        collect_closed_world_linked_modules_from_projection(
1604            inputs,
1605            resolved_dependencies,
1606            &instances_by_path,
1607            resolution_authority,
1608        )?;
1609    let module_metadata =
1610        module_metadata_with_reachability_evidence(module_metadata, module_reachability_evidence);
1611    let closed_world_bundle = ClosedWorldBundleV0::try_from_linked_modules_with_metadata(
1612        entrypoints.clone(),
1613        linked_modules,
1614        module_metadata,
1615    )
1616    .map_err(|error| TransformBundleLinkErrorV0::ClosedWorldBundle { error })?;
1617    Ok(PreparedLinkedStylesheetContextV0 {
1618        entrypoints,
1619        closed_world_bundle,
1620        dependency_resolution_disclosures,
1621    })
1622}
1623
1624fn link_stylesheet_from_projection_with_emission_items_and_metadata_and_options(
1625    entrypoint_paths: &[&str],
1626    linker_inputs: &[LinkerInputV0],
1627    emission_item_inputs: &[EmissionItemInputV0],
1628    resolved_dependencies: &[TransformBundleResolvedDependencyV0],
1629    module_metadata: &[ClosedWorldModuleMetadataV0],
1630    module_reachability_evidence: &BTreeMap<
1631        ModuleInstanceKeyV0,
1632        ClosedWorldModuleReachabilityEvidenceV0,
1633    >,
1634    options: TransformBundleLinkOptionsV0,
1635) -> Result<LinkedStylesheetWithEmissionItemsV0, TransformBundleLinkErrorV0> {
1636    let prepared = prepare_linked_stylesheet_context(
1637        entrypoint_paths,
1638        linker_inputs,
1639        resolved_dependencies,
1640        module_metadata,
1641        module_reachability_evidence,
1642        options.dependency_resolution_authority,
1643    )?;
1644    link_stylesheet_from_prepared_context_with_emission_items(
1645        prepared,
1646        linker_inputs,
1647        emission_item_inputs,
1648        resolved_dependencies,
1649        options,
1650    )
1651}
1652
1653fn evaluate_stylesheet_emission_admission_from_projection(
1654    entrypoint_paths: &[&str],
1655    linker_inputs: &[LinkerInputV0],
1656    emission_item_inputs: &[EmissionItemInputV0],
1657    resolved_dependencies: &[TransformBundleResolvedDependencyV0],
1658    module_metadata: &[ClosedWorldModuleMetadataV0],
1659    module_reachability_evidence: &BTreeMap<
1660        ModuleInstanceKeyV0,
1661        ClosedWorldModuleReachabilityEvidenceV0,
1662    >,
1663    options: TransformBundleLinkOptionsV0,
1664) -> TransformBundleEmissionAdmissionV0 {
1665    let prepared = match prepare_linked_stylesheet_context(
1666        entrypoint_paths,
1667        linker_inputs,
1668        resolved_dependencies,
1669        module_metadata,
1670        module_reachability_evidence,
1671        options.dependency_resolution_authority,
1672    ) {
1673        Ok(prepared) => prepared,
1674        Err(error) => {
1675            return TransformBundleEmissionAdmissionV0 {
1676                module_id_legacy_open: true,
1677                requested_policy_result: Err(error),
1678            };
1679        }
1680    };
1681    let module_id_legacy_open = build_linked_stylesheet_order_from_prepared_context(
1682        &prepared,
1683        linker_inputs,
1684        resolved_dependencies,
1685        TransformBundleLinkOptionsV0::default()
1686            .with_dependency_resolution_authority(options.dependency_resolution_authority),
1687    )
1688    .is_err();
1689    let requested_policy_result = link_stylesheet_from_prepared_context_with_emission_items(
1690        prepared,
1691        linker_inputs,
1692        emission_item_inputs,
1693        resolved_dependencies,
1694        options,
1695    );
1696    TransformBundleEmissionAdmissionV0 {
1697        module_id_legacy_open,
1698        requested_policy_result,
1699    }
1700}
1701
1702fn link_stylesheet_from_prepared_context_with_emission_items(
1703    prepared: PreparedLinkedStylesheetContextV0,
1704    linker_inputs: &[LinkerInputV0],
1705    emission_item_inputs: &[EmissionItemInputV0],
1706    resolved_dependencies: &[TransformBundleResolvedDependencyV0],
1707    options: TransformBundleLinkOptionsV0,
1708) -> Result<LinkedStylesheetWithEmissionItemsV0, TransformBundleLinkErrorV0> {
1709    let module_plan = emission_order::build_emission_module_plan(
1710        linker_inputs,
1711        prepared.closed_world_bundle.linked_modules(),
1712        &prepared.entrypoints,
1713        resolved_dependencies,
1714        options.emission_ordering_policy,
1715        options.dependency_resolution_authority,
1716    )?;
1717    let emission_plan =
1718        emission_order::build_emission_plan_from_module_plan(linker_inputs, &module_plan)?;
1719    let legacy_global_rule_order =
1720        emission_order::build_global_rule_order_from_plan(linker_inputs, &emission_plan)?;
1721    let emission_item_plan =
1722        emission_items::build_emission_item_plan(emission_item_inputs, &module_plan)?;
1723    let emission_item_order = emission_items::build_linked_emission_item_order(
1724        emission_item_inputs,
1725        &emission_item_plan,
1726    )?;
1727    let global_rule_order =
1728        emission_items::build_global_rule_order_from_emission_items(&emission_item_order)?;
1729    if global_rule_order != legacy_global_rule_order {
1730        return Err(TransformBundleLinkErrorV0::InvalidEmissionPlan {
1731            reason: "selector projection from emission items changed global rule order".to_string(),
1732        });
1733    }
1734    let projection_disclosures = emission_item_inputs
1735        .first()
1736        .map(|input| input.disclosure.clone())
1737        .unwrap_or_default();
1738    if emission_item_inputs
1739        .iter()
1740        .any(|input| input.disclosure != projection_disclosures)
1741    {
1742        return Err(TransformBundleLinkErrorV0::InvalidEmissionPlan {
1743            reason: "emission-item projection disclosure differs between modules".to_string(),
1744        });
1745    }
1746    let linked_stylesheet = LinkedStylesheetV0 {
1747        schema_version: "0",
1748        product: "omena-transform-bundle.linked-stylesheet",
1749        entrypoints: prepared.entrypoints,
1750        module_instances: prepared.closed_world_bundle.linked_modules().to_vec(),
1751        emission_plan,
1752        global_rule_order,
1753        closed_world_bundle: prepared.closed_world_bundle,
1754    };
1755    Ok(LinkedStylesheetWithEmissionItemsV0 {
1756        linked_stylesheet,
1757        emission_item_plan,
1758        emission_item_order,
1759        projection_disclosures,
1760        dependency_resolution_disclosures: prepared.dependency_resolution_disclosures,
1761    })
1762}
1763
1764pub fn rewrite_omena_transform_bundle_asset_urls_in_source(
1765    source_path: impl Into<String>,
1766    source: &str,
1767) -> TransformBundleAssetUrlRewriteSummaryV0 {
1768    let source_path = source_path.into();
1769    let asset_urls = collect_transform_ir_bundle_asset_urls(
1770        &source_path,
1771        source,
1772        dialect_for_bundle_source_path(&source_path),
1773    );
1774    let mut output_css = source.to_string();
1775    let mut rewritten_asset_urls = Vec::new();
1776
1777    for asset in asset_urls.iter().rev() {
1778        let Some(resolved_path) = asset.resolved_path.as_deref() else {
1779            continue;
1780        };
1781        if !asset.bundler_resolution_required || asset.normalized_url == resolved_path {
1782            continue;
1783        }
1784        let range_start = asset.range_start as usize;
1785        let range_end = asset.range_end as usize;
1786        if range_start > range_end || range_end > output_css.len() {
1787            continue;
1788        }
1789        output_css.replace_range(range_start..range_end, &format!("url(\"{resolved_path}\")"));
1790        rewritten_asset_urls.push(asset.clone());
1791    }
1792
1793    rewritten_asset_urls.reverse();
1794    TransformBundleAssetUrlRewriteSummaryV0 {
1795        schema_version: "0",
1796        product: "omena-transform-bundle.asset-url-rewrite",
1797        source_path,
1798        asset_url_count: asset_urls.len(),
1799        rewrite_count: rewritten_asset_urls.len(),
1800        output_css,
1801        rewritten_asset_urls,
1802    }
1803}
1804
1805fn linker_input_from_module_facts(
1806    source_path: &str,
1807    instance: ModuleInstanceKeyV0,
1808    facts: &ParsedStyleFacts,
1809    bundle_edges: &[TransformBundleEdgeV0],
1810) -> LinkerInputV0 {
1811    LinkerInputV0 {
1812        source_path: source_path.to_string(),
1813        instance,
1814        dependency_edges: bundle_edges
1815            .iter()
1816            .filter(|edge| bundle_edge_is_module_dependency(edge.kind))
1817            .filter_map(|edge| {
1818                edge.import_source
1819                    .as_ref()
1820                    .map(|import_source| LinkerDependencyEdgeV0 {
1821                        kind: edge.kind,
1822                        import_source: import_source.clone(),
1823                        import_ordinal: edge.import_ordinal,
1824                        local_names: edge.local_names.clone(),
1825                        remote_names: edge.remote_names.clone(),
1826                    })
1827            })
1828            .collect(),
1829        class_names: dedupe_names(
1830            facts
1831                .selectors
1832                .iter()
1833                .filter(|selector| selector.kind == ParsedSelectorFactKind::Class)
1834                .map(|selector| selector.name.clone()),
1835        ),
1836        keyframe_names: dedupe_names(
1837            facts
1838                .animations
1839                .iter()
1840                .filter(|animation| animation.kind == ParsedAnimationFactKind::KeyframesDeclaration)
1841                .map(|animation| animation.name.clone()),
1842        ),
1843        value_names: dedupe_names(
1844            facts
1845                .css_module_values
1846                .iter()
1847                .filter(|value| value.kind == ParsedCssModuleValueFactKind::Definition)
1848                .map(|value| value.name.clone()),
1849        ),
1850        custom_property_names: dedupe_names(
1851            facts
1852                .variables
1853                .iter()
1854                .filter(|variable| {
1855                    variable.kind == ParsedVariableFactKind::CustomPropertyDeclaration
1856                })
1857                .map(|variable| variable.name.clone()),
1858        ),
1859        ordered_rules: collect_ordered_linker_rules(facts),
1860    }
1861}
1862
1863fn collect_ordered_linker_rules(facts: &ParsedStyleFacts) -> Vec<LinkerRuleV0> {
1864    let mut selectors = facts.selectors.clone();
1865    selectors.sort_by_key(|selector| {
1866        (
1867            u32::from(selector.range.start()),
1868            u32::from(selector.range.end()),
1869            selector.kind,
1870            selector.name.clone(),
1871        )
1872    });
1873    selectors
1874        .into_iter()
1875        .map(|selector| LinkerRuleV0 {
1876            selector_name: selector.name,
1877            selector_kind: selector.kind,
1878            range_start: u32::from(selector.range.start()),
1879            range_end: u32::from(selector.range.end()),
1880        })
1881        .collect()
1882}
1883
1884#[allow(deprecated)]
1885fn fan_out_path_reachability_to_instances(
1886    modules: &[TransformBundleParsedModuleInputV0],
1887    reachability_inputs: &[TransformBundleSemanticReachabilityInputV0],
1888) -> Vec<TransformBundleInstanceReachabilityInputV0> {
1889    let instances_by_path = modules.iter().fold(
1890        BTreeMap::<String, Vec<ModuleInstanceKeyV0>>::new(),
1891        |mut by_path, module| {
1892            by_path
1893                .entry(normalize_bundle_path(PathBuf::from(module.source_path())))
1894                .or_default()
1895                .extend(module.module_instance_keys());
1896            by_path
1897        },
1898    );
1899    let mut reachability_by_path =
1900        BTreeMap::<String, TransformBundleSemanticReachabilityInputV0>::new();
1901    for input in reachability_inputs
1902        .iter()
1903        .filter(|input| input.has_reachable_symbols())
1904    {
1905        let normalized_path = normalize_bundle_path(PathBuf::from(&input.source_path));
1906        let merged = reachability_by_path
1907            .entry(normalized_path.clone())
1908            .or_insert_with(|| TransformBundleSemanticReachabilityInputV0::new(normalized_path));
1909        merged.class_names.extend(input.class_names.iter().cloned());
1910        merged
1911            .keyframe_names
1912            .extend(input.keyframe_names.iter().cloned());
1913        merged.value_names.extend(input.value_names.iter().cloned());
1914        merged
1915            .custom_property_names
1916            .extend(input.custom_property_names.iter().cloned());
1917        merged.class_names = dedupe_names(merged.class_names.drain(..));
1918        merged.keyframe_names = dedupe_names(merged.keyframe_names.drain(..));
1919        merged.value_names = dedupe_names(merged.value_names.drain(..));
1920        merged.custom_property_names = dedupe_names(merged.custom_property_names.drain(..));
1921    }
1922
1923    reachability_by_path
1924        .into_iter()
1925        .flat_map(|(path, reachability)| {
1926            instances_by_path
1927                .get(path.as_str())
1928                .into_iter()
1929                .flatten()
1930                .map(move |instance| {
1931                    let mut input = TransformBundleInstanceReachabilityInputV0::new(
1932                        instance.clone(),
1933                        InstanceReachabilityDerivationV0::PathUnionNoInstanceDiscriminator,
1934                    );
1935                    input.class_names.clone_from(&reachability.class_names);
1936                    input
1937                        .keyframe_names
1938                        .clone_from(&reachability.keyframe_names);
1939                    input.value_names.clone_from(&reachability.value_names);
1940                    input
1941                        .custom_property_names
1942                        .clone_from(&reachability.custom_property_names);
1943                    input
1944                })
1945        })
1946        .collect()
1947}
1948
1949fn apply_semantic_reachability_to_linker_inputs(
1950    inputs: &mut [LinkerInputV0],
1951    reachability_inputs: &[TransformBundleInstanceReachabilityInputV0],
1952) -> (
1953    BTreeMap<ModuleInstanceKeyV0, ClosedWorldModuleReachabilityEvidenceV0>,
1954    BTreeMap<ModuleInstanceKeyV0, InstanceReachabilityDerivationV0>,
1955) {
1956    let (reachability_inputs, incomplete_composes_target_instances) =
1957        instance_reachability_inputs_closed_over_composes(inputs, reachability_inputs);
1958    let module_index_by_instance = inputs
1959        .iter()
1960        .enumerate()
1961        .map(|(index, input)| (input.instance.clone(), index))
1962        .collect::<BTreeMap<_, _>>();
1963    let mut evidence_by_instance = inputs
1964        .iter()
1965        .map(|input| {
1966            (
1967                input.instance.clone(),
1968                ClosedWorldModuleReachabilityEvidenceV0::ModuleReachabilityInputAbsent,
1969            )
1970        })
1971        .collect::<BTreeMap<_, _>>();
1972    let mut derivation_by_instance = BTreeMap::new();
1973
1974    for input in reachability_inputs.values() {
1975        let Some(index) = module_index_by_instance
1976            .get(&input.module_instance)
1977            .copied()
1978        else {
1979            continue;
1980        };
1981        derivation_by_instance.insert(input.module_instance.clone(), input.derivation);
1982        if incomplete_composes_target_instances.contains(&input.module_instance) {
1983            // A composed-name carrier with a missing side cannot justify filtering its closure
1984            // target. Typed absence is attached to the module whose declarations could be lost.
1985            continue;
1986        }
1987        evidence_by_instance.insert(
1988            input.module_instance.clone(),
1989            ClosedWorldModuleReachabilityEvidenceV0::Supplied,
1990        );
1991        inputs[index].class_names.clear();
1992        inputs[index]
1993            .class_names
1994            .extend(input.class_names.iter().cloned());
1995        inputs[index].class_names = dedupe_names(inputs[index].class_names.drain(..));
1996        inputs[index].keyframe_names.clear();
1997        inputs[index]
1998            .keyframe_names
1999            .extend(input.keyframe_names.iter().cloned());
2000        inputs[index].keyframe_names = dedupe_names(inputs[index].keyframe_names.drain(..));
2001        inputs[index].value_names.clear();
2002        inputs[index]
2003            .value_names
2004            .extend(input.value_names.iter().cloned());
2005        inputs[index].value_names = dedupe_names(inputs[index].value_names.drain(..));
2006        inputs[index].custom_property_names.clear();
2007        inputs[index]
2008            .custom_property_names
2009            .extend(input.custom_property_names.iter().cloned());
2010        inputs[index].custom_property_names =
2011            dedupe_names(inputs[index].custom_property_names.drain(..));
2012    }
2013    (evidence_by_instance, derivation_by_instance)
2014}
2015
2016fn instance_reachability_inputs_closed_over_composes(
2017    inputs: &[LinkerInputV0],
2018    reachability_inputs: &[TransformBundleInstanceReachabilityInputV0],
2019) -> (
2020    BTreeMap<ModuleInstanceKeyV0, TransformBundleInstanceReachabilityInputV0>,
2021    BTreeSet<ModuleInstanceKeyV0>,
2022) {
2023    let instances_by_path = module_instances_by_linker_path(inputs);
2024    let mut by_instance =
2025        BTreeMap::<ModuleInstanceKeyV0, TransformBundleInstanceReachabilityInputV0>::new();
2026    let mut incomplete_composes_target_instances = BTreeSet::new();
2027    for input in reachability_inputs
2028        .iter()
2029        .filter(|input| input.has_reachable_symbols())
2030    {
2031        let merged = by_instance
2032            .entry(input.module_instance.clone())
2033            .or_insert_with(|| {
2034                TransformBundleInstanceReachabilityInputV0::new(
2035                    input.module_instance.clone(),
2036                    input.derivation,
2037                )
2038            });
2039        if input.derivation == InstanceReachabilityDerivationV0::PathUnionNoInstanceDiscriminator {
2040            merged.derivation = InstanceReachabilityDerivationV0::PathUnionNoInstanceDiscriminator;
2041        }
2042        merged.class_names.extend(input.class_names.iter().cloned());
2043        merged
2044            .keyframe_names
2045            .extend(input.keyframe_names.iter().cloned());
2046        merged.value_names.extend(input.value_names.iter().cloned());
2047        merged
2048            .custom_property_names
2049            .extend(input.custom_property_names.iter().cloned());
2050        merged.class_names = dedupe_names(merged.class_names.drain(..));
2051        merged.keyframe_names = dedupe_names(merged.keyframe_names.drain(..));
2052        merged.value_names = dedupe_names(merged.value_names.drain(..));
2053        merged.custom_property_names = dedupe_names(merged.custom_property_names.drain(..));
2054    }
2055
2056    loop {
2057        let snapshot = by_instance.clone();
2058        let mut additions = BTreeMap::<ModuleInstanceKeyV0, Vec<String>>::new();
2059        for input in inputs {
2060            let Some(source_reachability) = snapshot.get(&input.instance) else {
2061                continue;
2062            };
2063            for edge in input
2064                .dependency_edges
2065                .iter()
2066                .filter(|edge| edge.kind == TransformBundleEdgeKind::CssModuleComposesExternal)
2067            {
2068                let target_path =
2069                    import_path_candidates(input.source_path.as_str(), edge.import_source.as_str())
2070                        .into_iter()
2071                        .find(|candidate| instances_by_path.contains_key(candidate));
2072                let Some(target_path) = target_path else {
2073                    continue;
2074                };
2075                let Some(target_instances) = instances_by_path.get(&target_path) else {
2076                    continue;
2077                };
2078                if edge.local_names.is_empty() || edge.remote_names.is_empty() {
2079                    incomplete_composes_target_instances.extend(target_instances.iter().cloned());
2080                    continue;
2081                }
2082                for local_name in &edge.local_names {
2083                    if !source_reachability
2084                        .class_names
2085                        .iter()
2086                        .any(|reachable| reachable == local_name)
2087                    {
2088                        continue;
2089                    }
2090                    for remote_name in &edge.remote_names {
2091                        for target_instance in target_instances {
2092                            additions
2093                                .entry(target_instance.clone())
2094                                .or_default()
2095                                .push(remote_name.clone());
2096                        }
2097                    }
2098                }
2099            }
2100        }
2101        let mut changed = false;
2102        for (target_instance, class_names) in additions {
2103            let target = by_instance
2104                .entry(target_instance.clone())
2105                .or_insert_with(|| {
2106                    TransformBundleInstanceReachabilityInputV0::new(
2107                        target_instance,
2108                        InstanceReachabilityDerivationV0::PathUnionNoInstanceDiscriminator,
2109                    )
2110                });
2111            target.derivation = InstanceReachabilityDerivationV0::PathUnionNoInstanceDiscriminator;
2112            let before = target.class_names.len();
2113            target.class_names.extend(class_names);
2114            target.class_names = dedupe_names(target.class_names.drain(..));
2115            changed |= target.class_names.len() != before;
2116        }
2117        if !changed {
2118            break;
2119        }
2120    }
2121    (by_instance, incomplete_composes_target_instances)
2122}
2123
2124fn module_metadata_with_reachability_evidence(
2125    module_metadata: &[ClosedWorldModuleMetadataV0],
2126    module_reachability_evidence: &BTreeMap<
2127        ModuleInstanceKeyV0,
2128        ClosedWorldModuleReachabilityEvidenceV0,
2129    >,
2130) -> Vec<ClosedWorldModuleMetadataV0> {
2131    let mut metadata_by_instance = module_metadata
2132        .iter()
2133        .cloned()
2134        .map(|metadata| (metadata.module_instance().clone(), metadata))
2135        .collect::<BTreeMap<_, _>>();
2136    for (module_instance, reachability_evidence) in module_reachability_evidence {
2137        metadata_by_instance
2138            .entry(module_instance.clone())
2139            .and_modify(|metadata| {
2140                *metadata = metadata
2141                    .clone()
2142                    .with_reachability_evidence(*reachability_evidence);
2143            })
2144            .or_insert_with(|| {
2145                ClosedWorldModuleMetadataV0::new(module_instance.clone())
2146                    .with_reachability_evidence(*reachability_evidence)
2147            });
2148    }
2149    metadata_by_instance.into_values().collect()
2150}
2151
2152pub(crate) fn module_instances_by_linker_path(
2153    inputs: &[LinkerInputV0],
2154) -> BTreeMap<String, Vec<ModuleInstanceKeyV0>> {
2155    let mut by_path = BTreeMap::<String, Vec<ModuleInstanceKeyV0>>::new();
2156    for input in inputs {
2157        by_path
2158            .entry(input.source_path.clone())
2159            .or_default()
2160            .push(input.instance.clone());
2161    }
2162    for instances in by_path.values_mut() {
2163        instances.sort();
2164        instances.dedup();
2165    }
2166    by_path
2167}
2168
2169fn resolve_entrypoint_module_instance_by_path(
2170    source_path: &str,
2171    instances_by_path: &BTreeMap<String, Vec<ModuleInstanceKeyV0>>,
2172) -> Result<Option<ModuleInstanceKeyV0>, TransformBundleLinkErrorV0> {
2173    let normalized = normalize_bundle_path(PathBuf::from(source_path));
2174    let Some(instances) = instances_by_path.get(&normalized) else {
2175        return Ok(None);
2176    };
2177    match instances.as_slice() {
2178        [instance] => Ok(Some(instance.clone())),
2179        instances => {
2180            let unconfigured = omena_parser::ConfigurationHashV0::none();
2181            let mut matches = instances
2182                .iter()
2183                .filter(|instance| instance.configuration() == &unconfigured);
2184            let selected = matches.next().cloned();
2185            if selected.is_some() && matches.next().is_none() {
2186                Ok(selected)
2187            } else {
2188                Err(TransformBundleLinkErrorV0::AmbiguousModulePath {
2189                    source_path: normalized,
2190                })
2191            }
2192        }
2193    }
2194}
2195
2196fn collect_closed_world_linked_modules_from_projection(
2197    inputs: &[LinkerInputV0],
2198    resolved_dependencies: &[TransformBundleResolvedDependencyV0],
2199    instances_by_path: &BTreeMap<String, Vec<ModuleInstanceKeyV0>>,
2200    resolution_authority: BundleResolutionAuthorityV0,
2201) -> Result<
2202    (
2203        Vec<ClosedWorldLinkedModuleV0>,
2204        Vec<BundleDependencyResolutionDisclosureV0>,
2205    ),
2206    TransformBundleLinkErrorV0,
2207> {
2208    let linked_with_disclosures = inputs
2209        .iter()
2210        .map(|input| {
2211            let mut linked = ClosedWorldLinkedModuleV0::new(input.instance.clone());
2212            let mut disclosures = Vec::new();
2213            for edge in &input.dependency_edges {
2214                let resolution = resolve_imported_module_instance_for_edge(
2215                    input,
2216                    edge,
2217                    resolved_dependencies,
2218                    instances_by_path,
2219                    resolution_authority,
2220                )?;
2221                let dependency = resolution.target_instance.clone().ok_or_else(|| {
2222                    TransformBundleLinkErrorV0::MissingDependency {
2223                        source_path: input.source_path.clone(),
2224                        import_source: edge.import_source.clone(),
2225                    }
2226                })?;
2227                disclosures.push(BundleDependencyResolutionDisclosureV0 {
2228                    source_instance: input.instance.clone(),
2229                    import_source: edge.import_source.clone(),
2230                    import_ordinal: edge.import_ordinal,
2231                    authority: resolution.authority,
2232                });
2233                if edge.kind == TransformBundleEdgeKind::CssModuleComposesExternal {
2234                    for local_name in &edge.local_names {
2235                        for remote_name in &edge.remote_names {
2236                            linked = linked.with_composes_edge(ClosedWorldComposesEdgeV0 {
2237                                from_module: input.instance.clone(),
2238                                from_symbol: local_name.clone(),
2239                                to_module: dependency.clone(),
2240                                to_symbol: remote_name.clone(),
2241                            });
2242                        }
2243                    }
2244                }
2245                linked = linked.with_dependency(dependency);
2246            }
2247            for name in dedupe_names(input.class_names.iter().cloned()) {
2248                linked = linked.with_class_name(name);
2249            }
2250            for name in dedupe_names(input.keyframe_names.iter().cloned()) {
2251                linked = linked.with_keyframe_name(name);
2252            }
2253            for name in dedupe_names(input.value_names.iter().cloned()) {
2254                linked = linked.with_value_name(name);
2255            }
2256            for name in dedupe_names(input.custom_property_names.iter().cloned()) {
2257                linked = linked.with_custom_property_name(name);
2258            }
2259            linked.dependencies.sort();
2260            linked.dependencies.dedup();
2261            linked.composes_edges.sort_by(|left, right| {
2262                (
2263                    &left.from_module,
2264                    &left.from_symbol,
2265                    &left.to_module,
2266                    &left.to_symbol,
2267                )
2268                    .cmp(&(
2269                        &right.from_module,
2270                        &right.from_symbol,
2271                        &right.to_module,
2272                        &right.to_symbol,
2273                    ))
2274            });
2275            linked.composes_edges.dedup();
2276            linked.composes_edge_observation_count = linked.composes_edges.len();
2277            Ok((linked, disclosures))
2278        })
2279        .collect::<Result<Vec<_>, TransformBundleLinkErrorV0>>()?;
2280    let (linked_modules, disclosure_groups): (Vec<_>, Vec<_>) =
2281        linked_with_disclosures.into_iter().unzip();
2282    let mut disclosures = disclosure_groups.into_iter().flatten().collect::<Vec<_>>();
2283    disclosures.sort();
2284    disclosures.dedup();
2285    Ok((linked_modules, disclosures))
2286}
2287
2288const fn bundle_edge_module_dependency_reason(
2289    kind: TransformBundleEdgeKind,
2290) -> Option<&'static str> {
2291    match kind {
2292        TransformBundleEdgeKind::SassUse => Some("loads a Sass module instance"),
2293        TransformBundleEdgeKind::SassForward => Some("forwards a Sass module instance"),
2294        TransformBundleEdgeKind::SassImport => Some("loads Sass stylesheet rules"),
2295        TransformBundleEdgeKind::CssImport => Some("loads CSS stylesheet rules"),
2296        TransformBundleEdgeKind::LessImport => Some("loads Less stylesheet rules"),
2297        TransformBundleEdgeKind::CssModuleValueImport => Some("loads CSS Modules values"),
2298        TransformBundleEdgeKind::CssModuleComposesExternal => {
2299            Some("loads selectors from an external CSS Module")
2300        }
2301        TransformBundleEdgeKind::IcssImport => Some("loads ICSS values"),
2302        TransformBundleEdgeKind::CssModuleComposesLocal => None,
2303    }
2304}
2305
2306/// Returns whether an edge traverses into another stylesheet module.
2307///
2308/// Local CSS Modules composition stays outside this set because it names a
2309/// selector in the current module rather than a separately resolved source.
2310pub const fn bundle_edge_is_module_dependency(kind: TransformBundleEdgeKind) -> bool {
2311    bundle_edge_module_dependency_reason(kind).is_some()
2312}
2313
2314pub(crate) fn resolve_imported_module_instance(
2315    source_path: &str,
2316    import_source: &str,
2317    instances_by_path: &BTreeMap<String, Vec<ModuleInstanceKeyV0>>,
2318) -> Result<Option<ModuleInstanceKeyV0>, TransformBundleLinkErrorV0> {
2319    for candidate in import_path_candidates(source_path, import_source) {
2320        if let Some(instances) = instances_by_path.get(candidate.as_str()) {
2321            return match instances.as_slice() {
2322                [instance] => Ok(Some(instance.clone())),
2323                _ => Err(TransformBundleLinkErrorV0::AmbiguousModulePath {
2324                    source_path: candidate,
2325                }),
2326            };
2327        }
2328    }
2329    Ok(None)
2330}
2331
2332pub(crate) struct DependencyResolutionOutcomeV0 {
2333    pub(crate) target_instance: Option<ModuleInstanceKeyV0>,
2334    pub(crate) authority: BundleResolutionAuthorityV0,
2335}
2336
2337pub(crate) fn resolve_imported_module_instance_for_edge(
2338    input: &LinkerInputV0,
2339    edge: &LinkerDependencyEdgeV0,
2340    resolved_dependencies: &[TransformBundleResolvedDependencyV0],
2341    instances_by_path: &BTreeMap<String, Vec<ModuleInstanceKeyV0>>,
2342    resolution_authority: BundleResolutionAuthorityV0,
2343) -> Result<DependencyResolutionOutcomeV0, TransformBundleLinkErrorV0> {
2344    let mut matches = resolved_dependencies.iter().filter(|dependency| {
2345        dependency.source_instance == input.instance
2346            && dependency.edge_kind == edge.kind
2347            && dependency.import_source == edge.import_source
2348            && dependency.import_ordinal == edge.import_ordinal
2349    });
2350    if let Some(resolved) = matches.next() {
2351        if matches.next().is_some() {
2352            return Err(TransformBundleLinkErrorV0::InvalidEmissionPlan {
2353                reason: format!(
2354                    "dependency {} in {} has more than one resolved-edge record",
2355                    edge.import_source,
2356                    input.instance.module().as_str()
2357                ),
2358            });
2359        }
2360        return Ok(DependencyResolutionOutcomeV0 {
2361            target_instance: resolved
2362                .resolution
2363                .target_instance
2364                .as_ref()
2365                .and_then(|target| {
2366                    instances_by_path
2367                        .get(target.module().as_str())
2368                        .filter(|instances| instances.contains(target))
2369                        .map(|_| target.clone())
2370                }),
2371            authority: BundleResolutionAuthorityV0::Resolved,
2372        });
2373    }
2374    if resolution_authority == BundleResolutionAuthorityV0::Resolved {
2375        return Err(TransformBundleLinkErrorV0::UnresolvedDependencyEdge {
2376            source_path: input.source_path.clone(),
2377            import_source: edge.import_source.clone(),
2378            import_ordinal: edge.import_ordinal,
2379        });
2380    }
2381    Ok(DependencyResolutionOutcomeV0 {
2382        target_instance: resolve_imported_module_instance(
2383            input.source_path.as_str(),
2384            edge.import_source.as_str(),
2385            instances_by_path,
2386        )?,
2387        authority: BundleResolutionAuthorityV0::LegacyPathInferred,
2388    })
2389}
2390
2391fn import_path_candidates(source_path: &str, import_source: &str) -> Vec<String> {
2392    let base = if import_source.starts_with('/') {
2393        PathBuf::from(import_source)
2394    } else {
2395        Path::new(source_path)
2396            .parent()
2397            .unwrap_or_else(|| Path::new(""))
2398            .join(import_source)
2399    };
2400    let normalized = normalize_bundle_path(base);
2401    let mut candidates = vec![normalized.clone()];
2402    if Path::new(&normalized).extension().is_none() {
2403        for extension in ["css", "scss", "sass", "less"] {
2404            candidates.push(format!("{normalized}.{extension}"));
2405        }
2406        let path = Path::new(&normalized);
2407        if let Some(file_name) = path.file_name().and_then(|name| name.to_str()) {
2408            let mut partial = path.parent().unwrap_or_else(|| Path::new("")).to_path_buf();
2409            partial.push(format!("_{file_name}"));
2410            let partial = normalize_bundle_path(partial);
2411            for extension in ["scss", "sass"] {
2412                candidates.push(format!("{partial}.{extension}"));
2413            }
2414        }
2415    }
2416    candidates.sort();
2417    candidates.dedup();
2418    candidates
2419}
2420
2421pub(crate) fn selector_kind_label(kind: ParsedSelectorFactKind) -> &'static str {
2422    match kind {
2423        ParsedSelectorFactKind::Class => "class",
2424        ParsedSelectorFactKind::Id => "id",
2425        ParsedSelectorFactKind::Placeholder => "placeholder",
2426    }
2427}
2428
2429fn serialize_selector_fact_kind<S>(
2430    kind: &ParsedSelectorFactKind,
2431    serializer: S,
2432) -> Result<S::Ok, S::Error>
2433where
2434    S: serde::Serializer,
2435{
2436    serializer.serialize_str(selector_kind_label(*kind))
2437}
2438
2439fn dedupe_names(names: impl IntoIterator<Item = String>) -> Vec<String> {
2440    names
2441        .into_iter()
2442        .collect::<BTreeSet<_>>()
2443        .into_iter()
2444        .collect()
2445}
2446
2447fn collect_bundle_edges_from_facts(
2448    source_path: &str,
2449    dialect: StyleDialect,
2450    facts: &omena_parser::ParsedStyleFacts,
2451) -> Vec<TransformBundleEdgeV0> {
2452    let mut edges = Vec::new();
2453
2454    for edge in &facts.sass_module_edges {
2455        let kind = match edge.kind {
2456            ParsedSassModuleEdgeFactKind::Use => TransformBundleEdgeKind::SassUse,
2457            ParsedSassModuleEdgeFactKind::Forward => TransformBundleEdgeKind::SassForward,
2458            ParsedSassModuleEdgeFactKind::Import => import_edge_kind_for_dialect(dialect),
2459        };
2460        edges.push(TransformBundleEdgeV0 {
2461            kind,
2462            source_path: source_path.to_string(),
2463            import_source: Some(edge.source.clone()),
2464            import_ordinal: None,
2465            namespace: edge.namespace.clone(),
2466            local_names: Vec::new(),
2467            remote_names: Vec::new(),
2468            range_start: u32::from(edge.range.start()),
2469            range_end: u32::from(edge.range.end()),
2470            provenance_required: true,
2471        });
2472    }
2473
2474    for edge in &facts.css_module_value_import_edges {
2475        edges.push(TransformBundleEdgeV0 {
2476            kind: TransformBundleEdgeKind::CssModuleValueImport,
2477            source_path: source_path.to_string(),
2478            import_source: Some(edge.import_source.clone()),
2479            import_ordinal: None,
2480            namespace: None,
2481            local_names: vec![edge.local_name.clone()],
2482            remote_names: vec![edge.remote_name.clone()],
2483            range_start: u32::from(edge.range.start()),
2484            range_end: u32::from(edge.range.end()),
2485            provenance_required: true,
2486        });
2487    }
2488
2489    for edge in &facts.css_module_composes_edges {
2490        let kind = match edge.kind {
2491            ParsedCssModuleComposesEdgeKind::External => {
2492                TransformBundleEdgeKind::CssModuleComposesExternal
2493            }
2494            ParsedCssModuleComposesEdgeKind::Local | ParsedCssModuleComposesEdgeKind::Global => {
2495                TransformBundleEdgeKind::CssModuleComposesLocal
2496            }
2497        };
2498        edges.push(TransformBundleEdgeV0 {
2499            kind,
2500            source_path: source_path.to_string(),
2501            import_source: edge.import_source.clone(),
2502            import_ordinal: None,
2503            namespace: None,
2504            local_names: edge.owner_selector_names.clone(),
2505            remote_names: edge.target_names.clone(),
2506            range_start: u32::from(edge.range.start()),
2507            range_end: u32::from(edge.range.end()),
2508            provenance_required: true,
2509        });
2510    }
2511
2512    for edge in &facts.icss_import_edges {
2513        edges.push(TransformBundleEdgeV0 {
2514            kind: TransformBundleEdgeKind::IcssImport,
2515            source_path: source_path.to_string(),
2516            import_source: Some(edge.import_source.clone()),
2517            import_ordinal: None,
2518            namespace: None,
2519            local_names: vec![edge.local_name.clone()],
2520            remote_names: vec![edge.remote_name.clone()],
2521            range_start: u32::from(edge.range.start()),
2522            range_end: u32::from(edge.range.end()),
2523            provenance_required: true,
2524        });
2525    }
2526
2527    assign_parser_origin_import_ordinals(&mut edges);
2528    edges
2529}
2530
2531fn assign_parser_origin_import_ordinals(edges: &mut [TransformBundleEdgeV0]) {
2532    let mut order_bearing_indices = edges
2533        .iter()
2534        .enumerate()
2535        .filter(|(_, edge)| {
2536            edge.import_source.is_some()
2537                && edge.kind.order_relevance() == EdgeOrderRelevanceV0::OrderBearing
2538        })
2539        .map(|(index, edge)| (index, edge.range_start, edge.range_end))
2540        .collect::<Vec<_>>();
2541    order_bearing_indices
2542        .sort_by_key(|(index, range_start, range_end)| (*range_start, *range_end, *index));
2543    for (ordinal, (index, _, _)) in order_bearing_indices.into_iter().enumerate() {
2544        edges[index].import_ordinal = u32::try_from(ordinal).ok();
2545    }
2546}
2547
2548fn import_edge_kind_for_dialect(dialect: StyleDialect) -> TransformBundleEdgeKind {
2549    match dialect {
2550        StyleDialect::Css => TransformBundleEdgeKind::CssImport,
2551        StyleDialect::Less => TransformBundleEdgeKind::LessImport,
2552        StyleDialect::Scss | StyleDialect::Sass => TransformBundleEdgeKind::SassImport,
2553    }
2554}
2555
2556fn collect_transform_ir_bundle_asset_urls(
2557    source_path: &str,
2558    source: &str,
2559    dialect: StyleDialect,
2560) -> Vec<TransformBundleAssetUrlV0> {
2561    let ir = lower_transform_ir_from_source(source, dialect, source_path);
2562    ir.nodes
2563        .iter()
2564        .filter(|node| !node.deleted && node.kind == IrNodeKindV0::UrlValue)
2565        .filter_map(|url_value| {
2566            let start = url_value.source_span_start;
2567            let end = url_value.source_span_end;
2568            if start >= end
2569                || end > source.len()
2570                || !source.is_char_boundary(start)
2571                || !source.is_char_boundary(end)
2572            {
2573                return None;
2574            }
2575            let (raw_url, normalized_url, parsed_end) = parse_bundle_url_function(source, start)?;
2576            if parsed_end != end {
2577                return None;
2578            }
2579            let (kind, resolved_path) = classify_bundle_asset_url(source_path, &normalized_url);
2580            Some(TransformBundleAssetUrlV0 {
2581                source_path: source_path.to_string(),
2582                raw_url,
2583                normalized_url,
2584                kind,
2585                resolved_path,
2586                range_start: start as u32,
2587                range_end: parsed_end as u32,
2588                bundler_resolution_required: matches!(
2589                    kind,
2590                    TransformBundleAssetUrlKind::Relative
2591                        | TransformBundleAssetUrlKind::AbsolutePath
2592                ),
2593            })
2594        })
2595        .collect()
2596}
2597
2598#[cfg(test)]
2599fn raw_scan_bundle_asset_urls_for_oracle(
2600    source_path: &str,
2601    source: &str,
2602) -> Vec<TransformBundleAssetUrlV0> {
2603    let bytes = source.as_bytes();
2604    let mut urls = Vec::new();
2605    let mut index = 0usize;
2606
2607    while index + 4 <= bytes.len() {
2608        if !bytes[index].eq_ignore_ascii_case(&b'u')
2609            || !bytes[index + 1].eq_ignore_ascii_case(&b'r')
2610            || !bytes[index + 2].eq_ignore_ascii_case(&b'l')
2611            || bytes[index + 3] != b'('
2612        {
2613            index += 1;
2614            continue;
2615        }
2616        let Some((raw_url, normalized_url, end)) = parse_bundle_url_function(source, index) else {
2617            index += 4;
2618            continue;
2619        };
2620        let (kind, resolved_path) = classify_bundle_asset_url(source_path, &normalized_url);
2621        urls.push(TransformBundleAssetUrlV0 {
2622            source_path: source_path.to_string(),
2623            raw_url,
2624            normalized_url,
2625            kind,
2626            resolved_path,
2627            range_start: index as u32,
2628            range_end: end as u32,
2629            bundler_resolution_required: matches!(
2630                kind,
2631                TransformBundleAssetUrlKind::Relative | TransformBundleAssetUrlKind::AbsolutePath
2632            ),
2633        });
2634        index = end;
2635    }
2636
2637    urls
2638}
2639
2640fn dialect_for_bundle_source_path(source_path: &str) -> StyleDialect {
2641    let extension = Path::new(source_path)
2642        .extension()
2643        .and_then(|extension| extension.to_str())
2644        .unwrap_or_default()
2645        .to_ascii_lowercase();
2646    match extension.as_str() {
2647        "scss" => StyleDialect::Scss,
2648        "sass" => StyleDialect::Sass,
2649        "less" => StyleDialect::Less,
2650        _ => StyleDialect::Css,
2651    }
2652}
2653
2654fn parse_bundle_url_function(source: &str, start: usize) -> Option<(String, String, usize)> {
2655    let open_end = start.checked_add(4)?;
2656    let mut index = open_end;
2657    let mut quote = None;
2658    let mut escaped = false;
2659
2660    while index < source.len() {
2661        let ch = source[index..].chars().next()?;
2662        let next = index + ch.len_utf8();
2663        if let Some(active_quote) = quote {
2664            if escaped {
2665                escaped = false;
2666            } else if ch == '\\' {
2667                escaped = true;
2668            } else if ch == active_quote {
2669                quote = None;
2670            }
2671            index = next;
2672            continue;
2673        }
2674
2675        match ch {
2676            '"' | '\'' => quote = Some(ch),
2677            ')' => {
2678                let raw_url = source[start..next].to_string();
2679                let inner = source[open_end..index].trim();
2680                let normalized_url = unquote_bundle_url_inner(inner)?;
2681                return Some((raw_url, normalized_url, next));
2682            }
2683            _ => {}
2684        }
2685        index = next;
2686    }
2687
2688    None
2689}
2690
2691fn unquote_bundle_url_inner(inner: &str) -> Option<String> {
2692    if inner.is_empty() {
2693        return None;
2694    }
2695    let bytes = inner.as_bytes();
2696    if bytes.len() >= 2
2697        && ((bytes[0] == b'"' && bytes[bytes.len() - 1] == b'"')
2698            || (bytes[0] == b'\'' && bytes[bytes.len() - 1] == b'\''))
2699    {
2700        return Some(inner[1..inner.len() - 1].to_string());
2701    }
2702    Some(inner.to_string())
2703}
2704
2705fn classify_bundle_asset_url(
2706    source_path: &str,
2707    normalized_url: &str,
2708) -> (TransformBundleAssetUrlKind, Option<String>) {
2709    let lower = normalized_url.to_ascii_lowercase();
2710    if lower.starts_with("data:") {
2711        return (TransformBundleAssetUrlKind::Data, None);
2712    }
2713    if normalized_url.starts_with('#') {
2714        return (TransformBundleAssetUrlKind::Fragment, None);
2715    }
2716    if lower.starts_with("http://")
2717        || lower.starts_with("https://")
2718        || normalized_url.starts_with("//")
2719    {
2720        return (TransformBundleAssetUrlKind::External, None);
2721    }
2722    if normalized_url.starts_with('/') {
2723        return (
2724            TransformBundleAssetUrlKind::AbsolutePath,
2725            Some(normalized_url.to_string()),
2726        );
2727    }
2728
2729    (
2730        TransformBundleAssetUrlKind::Relative,
2731        Some(resolve_relative_bundle_asset_path(
2732            source_path,
2733            normalized_url,
2734        )),
2735    )
2736}
2737
2738fn resolve_relative_bundle_asset_path(source_path: &str, normalized_url: &str) -> String {
2739    let base = Path::new(source_path)
2740        .parent()
2741        .unwrap_or_else(|| Path::new(""));
2742    normalize_bundle_path(base.join(normalized_url))
2743}
2744
2745pub fn normalize_omena_transform_bundle_path(path: &str) -> String {
2746    normalize_bundle_path(PathBuf::from(path.replace('\\', "/"))).replace('\\', "/")
2747}
2748
2749fn normalize_bundle_path(path: PathBuf) -> String {
2750    let mut normalized = PathBuf::new();
2751    for component in path.components() {
2752        match component {
2753            Component::CurDir => {}
2754            Component::ParentDir => match normalized.components().next_back() {
2755                Some(Component::Normal(_)) => {
2756                    normalized.pop();
2757                }
2758                Some(Component::RootDir) => {}
2759                _ => normalized.push(".."),
2760            },
2761            _ => normalized.push(component.as_os_str()),
2762        }
2763    }
2764    normalized.to_string_lossy().into_owned()
2765}
2766
2767fn plan_bundle_code_split_chunks(
2768    source_path: &str,
2769    bundle_edges: &[TransformBundleEdgeV0],
2770    asset_urls: &[TransformBundleAssetUrlV0],
2771) -> Vec<TransformBundleChunkV0> {
2772    let mut chunks: Vec<TransformBundleChunkV0> = Vec::new();
2773    let mut entry_dependencies = Vec::new();
2774
2775    for edge in bundle_edges {
2776        let Some(import_source) = edge.import_source.as_ref() else {
2777            continue;
2778        };
2779        let chunk_id = bundle_chunk_id("style", source_path, import_source);
2780        if !entry_dependencies.contains(&chunk_id) {
2781            entry_dependencies.push(chunk_id.clone());
2782        }
2783        if chunks.iter().any(|chunk| chunk.chunk_id == chunk_id) {
2784            continue;
2785        }
2786        chunks.push(TransformBundleChunkV0 {
2787            chunk_id,
2788            kind: TransformBundleChunkKind::StyleImport,
2789            source_path: source_path.to_string(),
2790            import_source: Some(import_source.clone()),
2791            asset_url: None,
2792            resolved_path: None,
2793            depends_on: Vec::new(),
2794            split_boundary: "styleDependency",
2795        });
2796    }
2797
2798    for asset in asset_urls {
2799        if !asset.bundler_resolution_required {
2800            continue;
2801        }
2802        let chunk_id = bundle_chunk_id("asset", source_path, asset.normalized_url.as_str());
2803        if !entry_dependencies.contains(&chunk_id) {
2804            entry_dependencies.push(chunk_id.clone());
2805        }
2806        if chunks.iter().any(|chunk| chunk.chunk_id == chunk_id) {
2807            continue;
2808        }
2809        chunks.push(TransformBundleChunkV0 {
2810            chunk_id,
2811            kind: TransformBundleChunkKind::Asset,
2812            source_path: source_path.to_string(),
2813            import_source: None,
2814            asset_url: Some(asset.normalized_url.clone()),
2815            resolved_path: asset.resolved_path.clone(),
2816            depends_on: Vec::new(),
2817            split_boundary: "assetDependency",
2818        });
2819    }
2820
2821    entry_dependencies.sort();
2822    chunks.sort_by(|left, right| left.chunk_id.cmp(&right.chunk_id));
2823    let mut ordered = vec![TransformBundleChunkV0 {
2824        chunk_id: bundle_chunk_id("entry", source_path, source_path),
2825        kind: TransformBundleChunkKind::Entry,
2826        source_path: source_path.to_string(),
2827        import_source: None,
2828        asset_url: None,
2829        resolved_path: Some(source_path.to_string()),
2830        depends_on: entry_dependencies,
2831        split_boundary: "entry",
2832    }];
2833    ordered.extend(chunks);
2834    ordered
2835}
2836
2837fn bundle_chunk_id(kind: &str, source_path: &str, target: &str) -> String {
2838    format!(
2839        "{kind}:{}:{}",
2840        sanitize_bundle_chunk_id_part(source_path),
2841        sanitize_bundle_chunk_id_part(target)
2842    )
2843}
2844
2845fn sanitize_bundle_chunk_id_part(value: &str) -> String {
2846    let mut sanitized = String::with_capacity(value.len());
2847    for ch in value.chars() {
2848        if ch.is_ascii_alphanumeric() || matches!(ch, '-' | '_' | '.') {
2849            sanitized.push(ch);
2850        } else {
2851            sanitized.push('-');
2852        }
2853    }
2854    sanitized.trim_matches('-').to_string()
2855}
2856
2857fn required_passes_for_source(
2858    source_path: &str,
2859    dialect: StyleDialect,
2860    facts: &omena_parser::ParsedStyleFacts,
2861    bundle_edges: &[TransformBundleEdgeV0],
2862) -> Vec<TransformPassKind> {
2863    let mut passes = Vec::new();
2864
2865    if bundle_edges.iter().any(|edge| {
2866        matches!(
2867            edge.kind,
2868            TransformBundleEdgeKind::SassImport
2869                | TransformBundleEdgeKind::CssImport
2870                | TransformBundleEdgeKind::LessImport
2871                | TransformBundleEdgeKind::CssModuleValueImport
2872                | TransformBundleEdgeKind::CssModuleComposesExternal
2873                | TransformBundleEdgeKind::IcssImport
2874        )
2875    }) {
2876        passes.push(TransformPassKind::ImportInline);
2877    }
2878
2879    if matches!(dialect, StyleDialect::Scss | StyleDialect::Sass) {
2880        passes.push(TransformPassKind::ScssModuleEvaluate);
2881    }
2882
2883    if matches!(dialect, StyleDialect::Less) {
2884        passes.push(TransformPassKind::LessModuleEvaluate);
2885    }
2886
2887    if is_css_module_path(source_path) && facts.selector_count > 0 {
2888        passes.push(TransformPassKind::HashCssModuleClassNames);
2889    }
2890
2891    if facts.css_module_composes_edge_count > 0 {
2892        passes.push(TransformPassKind::ResolveCssModulesComposes);
2893    }
2894
2895    if facts.css_module_value_count > 0 || facts.css_module_value_import_edge_count > 0 {
2896        passes.push(TransformPassKind::ValueResolution);
2897    }
2898
2899    passes
2900}
2901
2902fn is_css_module_path(source_path: &str) -> bool {
2903    let file_name = source_path
2904        .rsplit(['/', '\\'])
2905        .next()
2906        .unwrap_or(source_path)
2907        .to_ascii_lowercase();
2908    let Some((stem, extension)) = file_name.rsplit_once('.') else {
2909        return false;
2910    };
2911    matches!(extension, "css" | "scss" | "sass" | "less") && stem.ends_with(".module")
2912}
2913
2914fn dialect_label(dialect: StyleDialect) -> &'static str {
2915    match dialect {
2916        StyleDialect::Css => "css",
2917        StyleDialect::Scss => "scss",
2918        StyleDialect::Sass => "sass",
2919        StyleDialect::Less => "less",
2920    }
2921}
2922
2923#[cfg(test)]
2924#[allow(deprecated)]
2925mod tests {
2926    use std::collections::{BTreeMap, BTreeSet};
2927
2928    use super::{
2929        InstanceReachabilityDerivationV0, LinkedStylesheetRuleV0, LinkerDependencyEdgeV0,
2930        LinkerInputV0, LinkerRuleV0, TRANSFORM_BUNDLE_EDGE_KIND_VARIANTS_V0,
2931        TransformBundleAssetUrlKind, TransformBundleChunkKind,
2932        TransformBundleDependencyResolutionV0, TransformBundleEdgeKind,
2933        TransformBundleInstanceReachabilityInputV0, TransformBundleLinkErrorV0,
2934        TransformBundleLinkOptionsV0, TransformBundleModuleInputV0,
2935        TransformBundleResolvedDependencyV0, TransformBundleSemanticReachabilityInputV0,
2936        TransformBundleTransformedModuleV0, apply_semantic_reachability_to_linker_inputs,
2937        bundle_edge_is_module_dependency, bundle_edge_module_dependency_reason,
2938        carrier_hygiene_assertions, collect_transform_ir_bundle_asset_urls,
2939        compare_omena_transform_bundle_emission_policies, link_omena_transform_bundle_modules,
2940        link_omena_transform_bundle_modules_with_options,
2941        link_omena_transform_bundle_modules_with_semantic_reachability,
2942        link_omena_transform_bundle_projection_with_resolved_dependencies_and_options,
2943        link_stylesheet_from_projection, materialize_omena_transform_bundle_linked_stylesheet,
2944        normalize_omena_transform_bundle_path, project_omena_transform_bundle_linker_inputs,
2945        raw_scan_bundle_asset_urls_for_oracle, rewrite_omena_transform_bundle_asset_urls_in_source,
2946        summarize_omena_transform_bundle_from_source,
2947    };
2948    use omena_cross_file_summary::EdgeOrderRelevanceV0;
2949    use omena_parser::{
2950        ClosedWorldModuleReachabilityEvidenceV0, ConfigurationHashV0, ModuleIdV0,
2951        ModuleInstanceKeyV0, ParsedSelectorFactKind, StyleDialect,
2952    };
2953
2954    #[test]
2955    fn public_path_normalizer_collapses_equivalent_cross_platform_spellings() {
2956        assert_eq!(
2957            normalize_omena_transform_bundle_path("/workspace/src/./nested/../Button.module.css"),
2958            "/workspace/src/Button.module.css"
2959        );
2960        assert_eq!(
2961            normalize_omena_transform_bundle_path(
2962                r"C:\workspace\src\.\nested\..\Button.module.css"
2963            ),
2964            "C:/workspace/src/Button.module.css"
2965        );
2966    }
2967
2968    #[test]
2969    fn builds_bundle_plan_from_scss_and_css_modules_parser_facts() {
2970        let source = r#"
2971@use "./tokens" as tokens;
2972@forward "./theme";
2973@value primary from "./colors.module.css";
2974.button {
2975  composes: reset from "./reset.module.css";
2976  color: tokens.$brand;
2977}
2978"#;
2979        let summary = summarize_omena_transform_bundle_from_source(
2980            "Button.module.scss",
2981            source,
2982            StyleDialect::Scss,
2983        );
2984
2985        assert_eq!(summary.product, "omena-transform-bundle.source");
2986        assert_eq!(summary.dialect, "scss");
2987        assert!(summary.import_inline_required);
2988        assert!(summary.module_evaluation_required);
2989        assert!(summary.css_modules_resolution_required);
2990        assert!(summary.class_hashing_required);
2991        assert!(summary.value_resolution_required);
2992        assert!(summary.pass_plan.violated_dag_edge_count == 0);
2993        assert!(summary.bundle_edges.iter().any(|edge| {
2994            edge.kind == TransformBundleEdgeKind::CssModuleComposesExternal
2995                && edge.import_source.as_deref() == Some("./reset.module.css")
2996        }));
2997        assert_eq!(
2998            summary.planned_pass_ids,
2999            vec![
3000                "import-inline",
3001                "scss-module-evaluate",
3002                "composes-resolution",
3003                "css-modules-class-hashing",
3004                "value-resolution"
3005            ]
3006        );
3007    }
3008
3009    #[test]
3010    fn bundle_edge_catalog_has_total_order_relevance() {
3011        assert_eq!(TRANSFORM_BUNDLE_EDGE_KIND_VARIANTS_V0.len(), 9);
3012        assert!(TRANSFORM_BUNDLE_EDGE_KIND_VARIANTS_V0.iter().all(|kind| {
3013            kind.order_relevance() == EdgeOrderRelevanceV0::OrderBearing
3014                && !kind.order_relevance_reason().is_empty()
3015        }));
3016    }
3017
3018    #[test]
3019    fn module_dependency_edge_authority_excludes_only_local_composition() {
3020        let module_dependencies = TRANSFORM_BUNDLE_EDGE_KIND_VARIANTS_V0
3021            .iter()
3022            .copied()
3023            .filter(|kind| bundle_edge_is_module_dependency(*kind))
3024            .collect::<Vec<_>>();
3025
3026        assert_eq!(module_dependencies.len(), 8);
3027        assert!(!module_dependencies.contains(&TransformBundleEdgeKind::CssModuleComposesLocal));
3028        assert!(TRANSFORM_BUNDLE_EDGE_KIND_VARIANTS_V0.iter().all(|kind| {
3029            bundle_edge_is_module_dependency(*kind)
3030                == bundle_edge_module_dependency_reason(*kind).is_some()
3031        }));
3032    }
3033
3034    #[test]
3035    fn recognizes_less_module_evaluation_from_dialect() {
3036        let summary = summarize_omena_transform_bundle_from_source(
3037            "Theme.module.less",
3038            r#"@import (reference) "tokens.less"; .card { color: @brand; }"#,
3039            StyleDialect::Less,
3040        );
3041
3042        assert!(summary.module_evaluation_required);
3043        assert!(summary.import_inline_required);
3044        assert!(
3045            summary
3046                .bundle_edges
3047                .iter()
3048                .any(|edge| edge.kind == TransformBundleEdgeKind::LessImport)
3049        );
3050        assert!(summary.required_pass_ids.contains(&"less-module-evaluate"));
3051        assert!(!summary.required_pass_ids.contains(&"scss-module-evaluate"));
3052        assert!(
3053            summary
3054                .required_pass_ids
3055                .contains(&"css-modules-class-hashing")
3056        );
3057    }
3058
3059    #[test]
3060    fn plans_plain_css_import_inline_without_scss_module_evaluation() {
3061        let summary = summarize_omena_transform_bundle_from_source(
3062            "App.css",
3063            r#"@import "./tokens.css"; .button { color: red; }"#,
3064            StyleDialect::Css,
3065        );
3066
3067        assert!(summary.import_inline_required);
3068        assert!(!summary.module_evaluation_required);
3069        assert_eq!(summary.required_pass_ids, vec!["import-inline"]);
3070        assert_eq!(summary.planned_pass_ids, vec!["import-inline"]);
3071        assert!(
3072            summary
3073                .bundle_edges
3074                .iter()
3075                .any(|edge| edge.kind == TransformBundleEdgeKind::CssImport)
3076        );
3077        assert!(
3078            !summary
3079                .bundle_edges
3080                .iter()
3081                .any(|edge| edge.kind == TransformBundleEdgeKind::SassImport)
3082        );
3083    }
3084
3085    #[test]
3086    fn rejects_module_substring_false_positive_paths() {
3087        let source = ".button { color: red; }";
3088        let backup_summary = summarize_omena_transform_bundle_from_source(
3089            "Button.module.backup.scss",
3090            source,
3091            StyleDialect::Scss,
3092        );
3093        let unrelated_summary = summarize_omena_transform_bundle_from_source(
3094            "module/Button.scss",
3095            source,
3096            StyleDialect::Scss,
3097        );
3098
3099        assert!(!backup_summary.class_hashing_required);
3100        assert!(!unrelated_summary.class_hashing_required);
3101        assert!(
3102            !backup_summary
3103                .required_pass_ids
3104                .contains(&"css-modules-class-hashing")
3105        );
3106        assert!(
3107            !unrelated_summary
3108                .required_pass_ids
3109                .contains(&"css-modules-class-hashing")
3110        );
3111    }
3112
3113    #[test]
3114    fn recognizes_css_module_path_by_final_stem_and_supported_extension() {
3115        let summary = summarize_omena_transform_bundle_from_source(
3116            "components\\Button.MODULE.SCSS",
3117            ".button { color: red; }",
3118            StyleDialect::Scss,
3119        );
3120
3121        assert!(summary.class_hashing_required);
3122        assert!(
3123            summary
3124                .required_pass_ids
3125                .contains(&"css-modules-class-hashing")
3126        );
3127    }
3128
3129    #[test]
3130    fn resolves_relative_asset_urls_from_source_path() {
3131        let summary = summarize_omena_transform_bundle_from_source(
3132            "src/components/Button.module.css",
3133            r#".button { background: url("../assets/icon.svg"); mask: url(/static/mask.svg); cursor: url(data:image/svg+xml,abc); filter: url(#shadow); border-image-source: URL(https://cdn.example.com/frame.png); }"#,
3134            StyleDialect::Css,
3135        );
3136
3137        assert_eq!(summary.asset_urls.len(), 5);
3138        assert!(summary.asset_urls.iter().any(|asset| {
3139            asset.normalized_url == "../assets/icon.svg"
3140                && asset.kind == TransformBundleAssetUrlKind::Relative
3141                && asset.resolved_path.as_deref() == Some("src/assets/icon.svg")
3142                && asset.bundler_resolution_required
3143        }));
3144        assert!(summary.asset_urls.iter().any(|asset| {
3145            asset.normalized_url == "/static/mask.svg"
3146                && asset.kind == TransformBundleAssetUrlKind::AbsolutePath
3147                && asset.resolved_path.as_deref() == Some("/static/mask.svg")
3148                && asset.bundler_resolution_required
3149        }));
3150
3151        assert!(summary.asset_urls.iter().any(|asset| {
3152            asset.kind == TransformBundleAssetUrlKind::Data && !asset.bundler_resolution_required
3153        }));
3154        assert!(summary.asset_urls.iter().any(|asset| {
3155            asset.kind == TransformBundleAssetUrlKind::Fragment
3156                && !asset.bundler_resolution_required
3157        }));
3158        assert!(summary.asset_urls.iter().any(|asset| {
3159            asset.kind == TransformBundleAssetUrlKind::External
3160                && !asset.bundler_resolution_required
3161        }));
3162    }
3163
3164    #[test]
3165    fn value_ir_asset_urls_match_raw_scan_byte_identical() {
3166        let corpus = [
3167            (
3168                "src/components/Button.module.css",
3169                StyleDialect::Css,
3170                r#".button { background: url("../assets/icon.svg"); mask: url(/static/mask.svg); cursor: url(data:image/svg+xml,abc); filter: url(#shadow); border-image-source: URL(https://cdn.example.com/frame.png); }"#,
3171            ),
3172            (
3173                "src/components/Card.module.scss",
3174                StyleDialect::Scss,
3175                r#".카드 { background-image: url(./img/아이콘.svg); }"#,
3176            ),
3177            (
3178                "src/components/Theme.module.less",
3179                StyleDialect::Less,
3180                r#".theme { background: url('../assets/theme.svg'); }"#,
3181            ),
3182        ];
3183
3184        for (source_path, dialect, source) in corpus {
3185            let transform_ir_urls =
3186                collect_transform_ir_bundle_asset_urls(source_path, source, dialect);
3187            let raw_urls = raw_scan_bundle_asset_urls_for_oracle(source_path, source);
3188            assert_eq!(transform_ir_urls, raw_urls, "{source_path}");
3189        }
3190    }
3191
3192    #[test]
3193    fn plans_code_split_chunks_for_style_and_asset_dependencies() {
3194        let summary = summarize_omena_transform_bundle_from_source(
3195            "src/components/Button.module.css",
3196            r#"@import "../theme.css"; .button { background: url("../assets/icon.svg"); }"#,
3197            StyleDialect::Css,
3198        );
3199
3200        assert!(summary.code_splitting_required);
3201        assert_eq!(summary.code_split_chunks.len(), 3);
3202        let entry_chunk_id = summary
3203            .code_split_chunks
3204            .iter()
3205            .find(|chunk| chunk.kind == TransformBundleChunkKind::Entry)
3206            .map(|chunk| {
3207                assert_eq!(chunk.split_boundary, "entry");
3208                assert_eq!(chunk.depends_on.len(), 2);
3209                chunk.chunk_id.clone()
3210            });
3211        assert!(entry_chunk_id.is_some());
3212
3213        let style_chunk_id = summary
3214            .code_split_chunks
3215            .iter()
3216            .find(|chunk| chunk.kind == TransformBundleChunkKind::StyleImport)
3217            .map(|chunk| {
3218                assert_eq!(chunk.import_source.as_deref(), Some("../theme.css"));
3219                assert_eq!(chunk.split_boundary, "styleDependency");
3220                chunk.chunk_id.clone()
3221            });
3222        assert!(style_chunk_id.is_some());
3223
3224        let asset_chunk_id = summary
3225            .code_split_chunks
3226            .iter()
3227            .find(|chunk| chunk.kind == TransformBundleChunkKind::Asset)
3228            .map(|chunk| {
3229                assert_eq!(chunk.asset_url.as_deref(), Some("../assets/icon.svg"));
3230                assert_eq!(chunk.resolved_path.as_deref(), Some("src/assets/icon.svg"));
3231                assert_eq!(chunk.split_boundary, "assetDependency");
3232                chunk.chunk_id.clone()
3233            });
3234        assert!(asset_chunk_id.is_some());
3235        let entry_dependencies = summary
3236            .code_split_chunks
3237            .iter()
3238            .find(|chunk| chunk.kind == TransformBundleChunkKind::Entry)
3239            .map(|chunk| chunk.depends_on.as_slice())
3240            .unwrap_or(&[]);
3241        assert!(style_chunk_id.is_some_and(|chunk_id| entry_dependencies.contains(&chunk_id)));
3242        assert!(asset_chunk_id.is_some_and(|chunk_id| entry_dependencies.contains(&chunk_id)));
3243    }
3244
3245    #[test]
3246    fn resolves_asset_urls_after_non_ascii_source_text() {
3247        let summary = summarize_omena_transform_bundle_from_source(
3248            "src/카드.module.css",
3249            ".카드 { background-image: url(./img/아이콘.svg); }",
3250            StyleDialect::Css,
3251        );
3252
3253        assert_eq!(summary.asset_urls.len(), 1);
3254        let asset = &summary.asset_urls[0];
3255        assert_eq!(asset.kind, TransformBundleAssetUrlKind::Relative);
3256        assert_eq!(asset.normalized_url, "./img/아이콘.svg");
3257        assert_eq!(asset.resolved_path.as_deref(), Some("src/img/아이콘.svg"));
3258    }
3259
3260    #[test]
3261    fn preserves_leading_parent_segments_without_source_parent() {
3262        let summary = summarize_omena_transform_bundle_from_source(
3263            "Button.module.css",
3264            ".button { background-image: url(../assets/icon.svg); }",
3265            StyleDialect::Css,
3266        );
3267
3268        assert_eq!(
3269            summary.asset_urls[0].resolved_path.as_deref(),
3270            Some("../assets/icon.svg")
3271        );
3272    }
3273
3274    #[test]
3275    fn rewrites_relative_asset_urls_to_resolved_bundle_paths() {
3276        let summary = rewrite_omena_transform_bundle_asset_urls_in_source(
3277            "src/components/Button.module.css",
3278            r#".button { background: url("../assets/icon.svg"); mask: url(/static/mask.svg); filter: url(#shadow); }"#,
3279        );
3280
3281        assert_eq!(summary.product, "omena-transform-bundle.asset-url-rewrite");
3282        assert_eq!(summary.asset_url_count, 3);
3283        assert_eq!(summary.rewrite_count, 1);
3284        assert!(summary.output_css.contains(r#"url("src/assets/icon.svg")"#));
3285        assert!(summary.output_css.contains("url(/static/mask.svg)"));
3286        assert!(summary.output_css.contains("url(#shadow)"));
3287        assert_eq!(
3288            summary
3289                .rewritten_asset_urls
3290                .first()
3291                .and_then(|asset| asset.resolved_path.as_deref()),
3292            Some("src/assets/icon.svg")
3293        );
3294    }
3295
3296    #[test]
3297    fn linker_global_rule_order_is_a_total_order_over_linked_rules() -> Result<(), String> {
3298        let modules = vec![
3299            TransformBundleModuleInputV0::new(
3300                "src/app.module.css",
3301                r#"@import "./theme.css"; .button { color: var(--brand); }"#,
3302                StyleDialect::Css,
3303            ),
3304            TransformBundleModuleInputV0::new(
3305                "src/theme.css",
3306                r#":root { --brand: red; } .theme { color: red; }"#,
3307                StyleDialect::Css,
3308            ),
3309        ];
3310
3311        let linked = link_omena_transform_bundle_modules(&["src/app.module.css"], &modules)
3312            .map_err(|err| format!("{err:?}"))?;
3313
3314        assert_eq!(linked.product, "omena-transform-bundle.linked-stylesheet");
3315        assert_eq!(linked.entrypoints.len(), 1);
3316        assert_eq!(linked.module_instances.len(), 2);
3317        assert_eq!(
3318            linked
3319                .global_rule_order
3320                .rules
3321                .iter()
3322                .map(|rule| rule.global_order_index)
3323                .collect::<Vec<_>>(),
3324            vec![0, 1]
3325        );
3326        assert!(
3327            linked
3328                .global_rule_order
3329                .rules
3330                .iter()
3331                .any(|rule| rule.selector_name == "button")
3332        );
3333        assert!(
3334            linked
3335                .closed_world_bundle
3336                .reachability()
3337                .class_names()
3338                .contains(&"theme".to_string())
3339        );
3340        assert!(
3341            linked
3342                .closed_world_bundle
3343                .reachability()
3344                .custom_property_names()
3345                .contains(&"--brand".to_string())
3346        );
3347        Ok(())
3348    }
3349
3350    #[test]
3351    fn emission_plan_is_the_only_rule_order_authority() -> Result<(), String> {
3352        let first = ModuleInstanceKeyV0::unconfigured(ModuleIdV0::new("a.css"));
3353        let second = ModuleInstanceKeyV0::unconfigured(ModuleIdV0::new("b.css"));
3354        let inputs = [
3355            LinkerInputV0 {
3356                source_path: "a.css".to_string(),
3357                instance: first.clone(),
3358                dependency_edges: Vec::new(),
3359                class_names: vec!["first".to_string()],
3360                keyframe_names: Vec::new(),
3361                value_names: Vec::new(),
3362                custom_property_names: Vec::new(),
3363                ordered_rules: vec![LinkerRuleV0 {
3364                    selector_name: "first".to_string(),
3365                    selector_kind: ParsedSelectorFactKind::Class,
3366                    range_start: 0,
3367                    range_end: 6,
3368                }],
3369            },
3370            LinkerInputV0 {
3371                source_path: "b.css".to_string(),
3372                instance: second.clone(),
3373                dependency_edges: Vec::new(),
3374                class_names: vec!["second".to_string()],
3375                keyframe_names: Vec::new(),
3376                value_names: Vec::new(),
3377                custom_property_names: Vec::new(),
3378                ordered_rules: vec![LinkerRuleV0 {
3379                    selector_name: "second".to_string(),
3380                    selector_kind: ParsedSelectorFactKind::Class,
3381                    range_start: 0,
3382                    range_end: 7,
3383                }],
3384            },
3385        ];
3386        let mut plan = super::emission_order::build_emission_plan(
3387            &inputs,
3388            &[first.clone(), second],
3389            &[first],
3390            &[],
3391            super::EmissionOrderingPolicyV0::ModuleIdLegacy,
3392            super::BundleResolutionAuthorityV0::LegacyPathInferred,
3393        )
3394        .map_err(|error| format!("{error:?}"))?;
3395        let original = super::emission_order::build_global_rule_order_from_plan(&inputs, &plan)
3396            .map_err(|error| format!("{error:?}"))?;
3397
3398        plan.entries.swap(0, 1);
3399        let perturbed = super::emission_order::build_global_rule_order_from_plan(&inputs, &plan)
3400            .map_err(|error| format!("{error:?}"))?;
3401
3402        assert_eq!(original.rules[0].selector_name, "first");
3403        assert_eq!(perturbed.rules[0].selector_name, "second");
3404        assert_ne!(original, perturbed);
3405        Ok(())
3406    }
3407
3408    #[test]
3409    fn parser_import_order_is_recorded_without_changing_default_output() -> Result<(), String> {
3410        fn link(imports: &str) -> Result<super::LinkedStylesheetV0, String> {
3411            link_omena_transform_bundle_modules(
3412                &["src/app.css"],
3413                &[
3414                    TransformBundleModuleInputV0::new(
3415                        "src/app.css",
3416                        format!("{imports} .app {{ color: red; }}"),
3417                        StyleDialect::Css,
3418                    ),
3419                    TransformBundleModuleInputV0::new(
3420                        "src/a.css",
3421                        ".a { color: blue; }",
3422                        StyleDialect::Css,
3423                    ),
3424                    TransformBundleModuleInputV0::new(
3425                        "src/z.css",
3426                        ".z { color: green; }",
3427                        StyleDialect::Css,
3428                    ),
3429                ],
3430            )
3431            .map_err(|error| format!("{error:?}"))
3432        }
3433
3434        let a_then_z = link(r#"@import "./a.css"; @import "./z.css";"#)?;
3435        let z_then_a = link(r#"@import "./z.css"; @import "./a.css";"#)?;
3436        let targets = |linked: &super::LinkedStylesheetV0| {
3437            linked
3438                .emission_plan
3439                .dependency_facts
3440                .iter()
3441                .map(|fact| fact.to_module.module().as_str().to_string())
3442                .collect::<Vec<_>>()
3443        };
3444
3445        assert_eq!(targets(&a_then_z), vec!["src/a.css", "src/z.css"]);
3446        assert_eq!(targets(&z_then_a), vec!["src/z.css", "src/a.css"]);
3447        assert_eq!(
3448            serde_json::to_vec(&a_then_z).map_err(|error| format!("{error:?}"))?,
3449            serde_json::to_vec(&z_then_a).map_err(|error| format!("{error:?}"))?
3450        );
3451        Ok(())
3452    }
3453
3454    #[test]
3455    fn default_emission_policy_is_pinned_to_legacy_module_id_order() -> Result<(), String> {
3456        let modules = [
3457            TransformBundleModuleInputV0::new(
3458                "src/app.css",
3459                r#"@import "./z.css"; .app { color: red; }"#,
3460                StyleDialect::Css,
3461            ),
3462            TransformBundleModuleInputV0::new(
3463                "src/z.css",
3464                ".z { color: green; }",
3465                StyleDialect::Css,
3466            ),
3467        ];
3468        let implicit = link_omena_transform_bundle_modules(&["src/app.css"], &modules)
3469            .map_err(|error| format!("{error:?}"))?;
3470        let explicit = link_omena_transform_bundle_modules_with_options(
3471            &["src/app.css"],
3472            &modules,
3473            &[],
3474            &[],
3475            TransformBundleLinkOptionsV0 {
3476                emission_ordering_policy: super::EmissionOrderingPolicyV0::ModuleIdLegacy,
3477                ..TransformBundleLinkOptionsV0::default()
3478            },
3479        )
3480        .map_err(|error| format!("{error:?}"))?;
3481
3482        assert_eq!(
3483            implicit.emission_plan.policy,
3484            super::EmissionOrderingPolicyV0::ModuleIdLegacy
3485        );
3486        assert_eq!(
3487            serde_json::to_vec(&implicit).map_err(|error| format!("{error:?}"))?,
3488            serde_json::to_vec(&explicit).map_err(|error| format!("{error:?}"))?
3489        );
3490        Ok(())
3491    }
3492
3493    #[test]
3494    fn import_order_policy_reports_real_output_differences() -> Result<(), String> {
3495        let modules = [
3496            TransformBundleModuleInputV0::new(
3497                "src/app.css",
3498                r#"@import "./z.css"; @import "./a.css"; .app { color: red; }"#,
3499                StyleDialect::Css,
3500            ),
3501            TransformBundleModuleInputV0::new(
3502                "src/a.css",
3503                ".a { color: blue; }",
3504                StyleDialect::Css,
3505            ),
3506            TransformBundleModuleInputV0::new(
3507                "src/z.css",
3508                ".z { color: green; }",
3509                StyleDialect::Css,
3510            ),
3511        ];
3512        let linked = link_omena_transform_bundle_modules_with_options(
3513            &["src/app.css"],
3514            &modules,
3515            &[],
3516            &[],
3517            TransformBundleLinkOptionsV0 {
3518                emission_ordering_policy: super::EmissionOrderingPolicyV0::ImportOrderPreserving,
3519                ..TransformBundleLinkOptionsV0::default()
3520            },
3521        )
3522        .map_err(|error| format!("{error:?}"))?;
3523        let report = compare_omena_transform_bundle_emission_policies(&["src/app.css"], &modules)
3524            .map_err(|error| format!("{error:?}"))?;
3525
3526        assert_eq!(
3527            linked
3528                .global_rule_order
3529                .rules
3530                .iter()
3531                .map(|rule| rule.selector_name.as_str())
3532                .collect::<Vec<_>>(),
3533            vec!["z", "a", "app"]
3534        );
3535        assert!(!report.equivalent);
3536        assert_eq!(report.difference_count, report.differences.len());
3537        assert!(report.difference_count >= 2);
3538        Ok(())
3539    }
3540
3541    #[test]
3542    fn linked_emission_materializes_the_global_module_order() -> Result<(), String> {
3543        let modules = [
3544            TransformBundleModuleInputV0::new(
3545                "src/app.css",
3546                r#"@import "./z.css"; @import "./a.css"; .app { color: red; }"#,
3547                StyleDialect::Css,
3548            ),
3549            TransformBundleModuleInputV0::new(
3550                "src/a.css",
3551                ".a { color: blue; }",
3552                StyleDialect::Css,
3553            ),
3554            TransformBundleModuleInputV0::new(
3555                "src/z.css",
3556                ".z { color: green; }",
3557                StyleDialect::Css,
3558            ),
3559        ];
3560        let link = |policy| {
3561            link_omena_transform_bundle_modules_with_options(
3562                &["src/app.css"],
3563                &modules,
3564                &[],
3565                &[],
3566                TransformBundleLinkOptionsV0 {
3567                    emission_ordering_policy: policy,
3568                    ..TransformBundleLinkOptionsV0::default()
3569                },
3570            )
3571            .map_err(|error| format!("{error:?}"))
3572        };
3573        let legacy = link(super::EmissionOrderingPolicyV0::ModuleIdLegacy)?;
3574        let import_order = link(super::EmissionOrderingPolicyV0::ImportOrderPreserving)?;
3575        let transformed_modules = legacy
3576            .module_instances
3577            .iter()
3578            .cloned()
3579            .map(|module_instance| {
3580                let marker = module_instance.module().as_str().replace(['/', '.'], "-");
3581                TransformBundleTransformedModuleV0::new(
3582                    module_instance,
3583                    format!(".{marker} {{ order: linked; }}"),
3584                )
3585            })
3586            .collect::<Vec<_>>();
3587
3588        let legacy_output =
3589            materialize_omena_transform_bundle_linked_stylesheet(&legacy, &transformed_modules)
3590                .map_err(|error| format!("{error:?}"))?;
3591        let import_order_output = materialize_omena_transform_bundle_linked_stylesheet(
3592            &import_order,
3593            &transformed_modules,
3594        )
3595        .map_err(|error| format!("{error:?}"))?;
3596
3597        assert_ne!(legacy_output.output_css, import_order_output.output_css);
3598        assert_eq!(
3599            import_order_output
3600                .module_regions
3601                .iter()
3602                .map(|region| region.module_instance.module().as_str())
3603                .collect::<Vec<_>>(),
3604            vec!["src/z.css", "src/a.css", "src/app.css"]
3605        );
3606        assert_eq!(import_order_output.emitted_module_count, 3);
3607        assert_eq!(
3608            import_order_output.global_order_entry_count,
3609            import_order.global_rule_order.rules.len()
3610        );
3611        for transformed in &transformed_modules {
3612            assert_eq!(
3613                import_order_output
3614                    .output_css
3615                    .matches(&transformed.output_css)
3616                    .count(),
3617                1,
3618                "each transformed module must be emitted exactly once"
3619            );
3620        }
3621        for entry_region in &import_order_output.order_entry_regions {
3622            let module_region = import_order_output
3623                .module_regions
3624                .iter()
3625                .find(|region| region.module_instance == entry_region.module_instance)
3626                .ok_or_else(|| "ordered entry has no generated module region".to_string())?;
3627            assert_eq!(entry_region.generated_start, module_region.generated_start);
3628            assert_eq!(entry_region.generated_end, module_region.generated_end);
3629        }
3630        Ok(())
3631    }
3632
3633    #[test]
3634    fn linked_emission_rejects_preinlined_module_bytes() -> Result<(), String> {
3635        let modules = [
3636            TransformBundleModuleInputV0::new(
3637                "src/app.css",
3638                r#"@import "./theme.css"; .app { color: red; }"#,
3639                StyleDialect::Css,
3640            ),
3641            TransformBundleModuleInputV0::new(
3642                "src/theme.css",
3643                ".theme { color: blue; }",
3644                StyleDialect::Css,
3645            ),
3646        ];
3647        let linked = link_omena_transform_bundle_modules(&["src/app.css"], &modules)
3648            .map_err(|error| format!("{error:?}"))?;
3649        let transformed_modules = linked
3650            .module_instances
3651            .iter()
3652            .cloned()
3653            .enumerate()
3654            .map(|(index, module_instance)| {
3655                TransformBundleTransformedModuleV0::new(
3656                    module_instance,
3657                    format!(".module-{index} {{ order: linked; }}"),
3658                )
3659                .with_non_empty_import_replacement_count(usize::from(index == 0))
3660            })
3661            .collect::<Vec<_>>();
3662
3663        let result =
3664            materialize_omena_transform_bundle_linked_stylesheet(&linked, &transformed_modules);
3665
3666        assert!(matches!(
3667            result,
3668            Err(
3669                super::LinkedEmissionMaterializationErrorV0::ImportReplacementWouldDuplicateModule {
3670                    replacement_count: 1,
3671                    ..
3672                }
3673            )
3674        ));
3675        Ok(())
3676    }
3677
3678    #[test]
3679    fn dependency_cycles_are_recorded_with_an_explicit_policy() -> Result<(), String> {
3680        let first = ModuleInstanceKeyV0::unconfigured(ModuleIdV0::new("a.css"));
3681        let second = ModuleInstanceKeyV0::unconfigured(ModuleIdV0::new("b.css"));
3682        let input = |source_path: &str,
3683                     instance: ModuleInstanceKeyV0,
3684                     import_source: &str,
3685                     selector: &str| LinkerInputV0 {
3686            source_path: source_path.to_string(),
3687            instance,
3688            dependency_edges: vec![LinkerDependencyEdgeV0 {
3689                kind: TransformBundleEdgeKind::CssImport,
3690                import_source: import_source.to_string(),
3691                import_ordinal: Some(0),
3692                local_names: Vec::new(),
3693                remote_names: Vec::new(),
3694            }],
3695            class_names: vec![selector.to_string()],
3696            keyframe_names: Vec::new(),
3697            value_names: Vec::new(),
3698            custom_property_names: Vec::new(),
3699            ordered_rules: vec![LinkerRuleV0 {
3700                selector_name: selector.to_string(),
3701                selector_kind: ParsedSelectorFactKind::Class,
3702                range_start: 0,
3703                range_end: selector.len() as u32,
3704            }],
3705        };
3706        let linked = link_stylesheet_from_projection(
3707            &["a.css"],
3708            &[
3709                input("a.css", first, "./b.css", "a"),
3710                input("b.css", second, "./a.css", "b"),
3711            ],
3712        )
3713        .map_err(|error| format!("{error:?}"))?;
3714
3715        assert_eq!(linked.emission_plan.cycle_groups.len(), 1);
3716        let group = &linked.emission_plan.cycle_groups[0];
3717        assert_eq!(group.class, super::EmissionCycleClassV0::Import);
3718        assert_eq!(group.policy, super::EmissionCyclePolicyV0::ModuleIdentity);
3719        assert_eq!(group.members, group.chosen_order);
3720        Ok(())
3721    }
3722
3723    #[test]
3724    fn unsupported_module_cycle_edge_fails_closed() {
3725        let first = ModuleInstanceKeyV0::unconfigured(ModuleIdV0::new("a.css"));
3726        let second = ModuleInstanceKeyV0::unconfigured(ModuleIdV0::new("b.css"));
3727        let input =
3728            |source_path: &str, instance: ModuleInstanceKeyV0, import_source: &str| LinkerInputV0 {
3729                source_path: source_path.to_string(),
3730                instance,
3731                dependency_edges: vec![LinkerDependencyEdgeV0 {
3732                    kind: TransformBundleEdgeKind::CssModuleComposesLocal,
3733                    import_source: import_source.to_string(),
3734                    import_ordinal: Some(0),
3735                    local_names: Vec::new(),
3736                    remote_names: Vec::new(),
3737                }],
3738                class_names: Vec::new(),
3739                keyframe_names: Vec::new(),
3740                value_names: Vec::new(),
3741                custom_property_names: Vec::new(),
3742                ordered_rules: Vec::new(),
3743            };
3744
3745        let result = link_stylesheet_from_projection(
3746            &["a.css"],
3747            &[
3748                input("a.css", first, "./b.css"),
3749                input("b.css", second, "./a.css"),
3750            ],
3751        );
3752
3753        assert_eq!(
3754            result,
3755            Err(TransformBundleLinkErrorV0::UnsupportedEmissionCycle {
3756                edge_kind: TransformBundleEdgeKind::CssModuleComposesLocal,
3757            })
3758        );
3759    }
3760
3761    #[test]
3762    fn public_cascade_key_helper_normalizes_the_layer_ordinal() {
3763        let rule = LinkedStylesheetRuleV0 {
3764            global_order_index: 7,
3765            module_instance: ModuleInstanceKeyV0::unconfigured(ModuleIdV0::new("entry.css")),
3766            selector_name: "target".to_string(),
3767            selector_kind: "class",
3768            range_start: 0,
3769            range_end: 7,
3770        };
3771        let layer_ordinal = omena_cascade::LayerOrdinal::new(2);
3772        assert_eq!(layer_ordinal.map(omena_cascade::LayerOrdinal::get), Some(2));
3773        let Some(layer_ordinal) = layer_ordinal else {
3774            return;
3775        };
3776        let module_rank = omena_cascade::ModuleRank::new(3, 2, 1);
3777        let (key, open_world_tie_evidence) = rule.cascade_key_with_global_source_order(
3778            omena_cascade::CascadeLevel::AuthorNormal,
3779            layer_ordinal,
3780            false,
3781            0,
3782            omena_cascade::Specificity::new(0, 1, 0),
3783            module_rank,
3784        );
3785
3786        assert_eq!(
3787            key.layer_rank,
3788            omena_cascade::normalized_layer_rank(false, Some(layer_ordinal))
3789        );
3790        assert_eq!(key.source_order, 7);
3791        assert_eq!(open_world_tie_evidence.module_rank, module_rank);
3792    }
3793
3794    #[test]
3795    fn cascade_source_order_is_fed_by_global_rule_order() -> Result<(), String> {
3796        let modules = vec![
3797            TransformBundleModuleInputV0::new(
3798                "src/app.module.css",
3799                r#"@import "./theme.css"; .button { color: red; }"#,
3800                StyleDialect::Css,
3801            ),
3802            TransformBundleModuleInputV0::new(
3803                "src/theme.css",
3804                r#".button { color: blue; }"#,
3805                StyleDialect::Css,
3806            ),
3807        ];
3808
3809        let linked = link_omena_transform_bundle_modules(&["src/app.module.css"], &modules)
3810            .map_err(|err| format!("{err:?}"))?;
3811        let button_rules = linked
3812            .global_rule_order
3813            .rules
3814            .iter()
3815            .filter(|rule| rule.selector_name == "button")
3816            .collect::<Vec<_>>();
3817
3818        assert_eq!(button_rules.len(), 2);
3819        assert_eq!(
3820            button_rules
3821                .iter()
3822                .map(|rule| rule.global_order_index)
3823                .collect::<Vec<_>>(),
3824            vec![0, 1]
3825        );
3826
3827        let Some(layer_ordinal) = omena_cascade::LayerOrdinal::new(0) else {
3828            return Err("zero must remain a sentinel-safe layer ordinal".to_string());
3829        };
3830        let declarations = button_rules
3831            .iter()
3832            .map(|rule| {
3833                let value = if rule.global_order_index == 0 {
3834                    "red"
3835                } else {
3836                    "blue"
3837                };
3838                let (key, open_world_tie_evidence) = rule.cascade_key_with_global_source_order(
3839                    omena_cascade::CascadeLevel::AuthorNormal,
3840                    layer_ordinal,
3841                    false,
3842                    0,
3843                    omena_cascade::Specificity::new(0, 1, 0),
3844                    if rule.global_order_index == 0 {
3845                        omena_cascade::ModuleRank::new(u32::MAX, u32::MAX, u32::MAX)
3846                    } else {
3847                        omena_cascade::ModuleRank::ZERO
3848                    },
3849                );
3850                omena_cascade::CascadeDeclaration {
3851                    id: format!(
3852                        "{}:{}",
3853                        rule.module_instance.module().as_str(),
3854                        rule.global_order_index
3855                    ),
3856                    property: "color".to_string(),
3857                    value: omena_cascade::CascadeValue::Literal(value.to_string()),
3858                    key,
3859                    open_world_tie_evidence,
3860                    specificity_exactness: omena_cascade::SpecificityExactnessV0::Exact,
3861                }
3862            })
3863            .collect::<Vec<_>>();
3864
3865        let outcome = omena_cascade::cascade_property(declarations, "color");
3866        let omena_cascade::CascadeOutcome::Definite { winner, proof, .. } = outcome else {
3867            return Err("expected definite cascade winner".to_string());
3868        };
3869        assert_eq!(
3870            winner.value,
3871            omena_cascade::CascadeValue::Literal("blue".to_string())
3872        );
3873        assert_eq!(winner.key.source_order, 1);
3874        assert_eq!(proof.source_order, 1);
3875        Ok(())
3876    }
3877
3878    #[test]
3879    fn cascade_closed_world_order_matches_module_rank_key_byte_identical() -> Result<(), String> {
3880        let modules = vec![
3881            TransformBundleModuleInputV0::new(
3882                "src/app.module.css",
3883                r#"@import "./theme.css"; .button { color: red; }"#,
3884                StyleDialect::Css,
3885            ),
3886            TransformBundleModuleInputV0::new(
3887                "src/theme.css",
3888                r#".button { color: blue; }"#,
3889                StyleDialect::Css,
3890            ),
3891        ];
3892
3893        let linked = link_omena_transform_bundle_modules(&["src/app.module.css"], &modules)
3894            .map_err(|err| format!("{err:?}"))?;
3895        let Some(layer_ordinal) = omena_cascade::LayerOrdinal::new(0) else {
3896            return Err("zero must remain a sentinel-safe layer ordinal".to_string());
3897        };
3898        let declarations = linked
3899            .global_rule_order
3900            .rules
3901            .iter()
3902            .filter(|rule| rule.selector_name == "button")
3903            .map(|rule| {
3904                let linked_later = rule.global_order_index == 1;
3905                let (key, open_world_tie_evidence) = rule.cascade_key_with_global_source_order(
3906                    omena_cascade::CascadeLevel::AuthorNormal,
3907                    layer_ordinal,
3908                    false,
3909                    0,
3910                    omena_cascade::Specificity::new(0, 1, 0),
3911                    if linked_later {
3912                        omena_cascade::ModuleRank::new(u32::MAX, u32::MAX, u32::MAX)
3913                    } else {
3914                        omena_cascade::ModuleRank::ZERO
3915                    },
3916                );
3917                omena_cascade::CascadeDeclaration {
3918                    id: format!(
3919                        "{}:{}",
3920                        rule.module_instance.module().as_str(),
3921                        rule.global_order_index
3922                    ),
3923                    property: "color".to_string(),
3924                    value: omena_cascade::CascadeValue::Literal(if linked_later {
3925                        "blue".to_string()
3926                    } else {
3927                        "red".to_string()
3928                    }),
3929                    key,
3930                    open_world_tie_evidence,
3931                    specificity_exactness: omena_cascade::SpecificityExactnessV0::Exact,
3932                }
3933            })
3934            .collect::<Vec<_>>();
3935
3936        let linked_order_css = definite_color_css(omena_cascade::cascade_property(
3937            declarations.clone(),
3938            "color",
3939        ))?;
3940        let module_rank_keyed_css = legacy_module_rank_keyed_color_css(&declarations)?;
3941
3942        assert_eq!(
3943            linked_order_css.as_bytes(),
3944            module_rank_keyed_css.as_bytes()
3945        );
3946        Ok(())
3947    }
3948
3949    fn definite_color_css(outcome: omena_cascade::CascadeOutcome) -> Result<String, String> {
3950        let omena_cascade::CascadeOutcome::Definite { winner, .. } = outcome else {
3951            return Err("expected definite cascade winner".to_string());
3952        };
3953        let omena_cascade::CascadeValue::Literal(value) = winner.value else {
3954            return Err("expected literal cascade value".to_string());
3955        };
3956        Ok(format!("color:{value};"))
3957    }
3958
3959    fn legacy_module_rank_keyed_color_css(
3960        declarations: &[omena_cascade::CascadeDeclaration],
3961    ) -> Result<String, String> {
3962        let mut matching = declarations.to_vec();
3963        matching.sort_by(|left, right| {
3964            legacy_module_rank_key(right)
3965                .cmp(&legacy_module_rank_key(left))
3966                .then_with(|| right.key.source_order.cmp(&left.key.source_order))
3967        });
3968        let Some(winner) = matching.first() else {
3969            return Err("expected cascade declarations".to_string());
3970        };
3971        let omena_cascade::CascadeValue::Literal(value) = &winner.value else {
3972            return Err("expected literal cascade value".to_string());
3973        };
3974        Ok(format!("color:{value};"))
3975    }
3976
3977    fn legacy_module_rank_key(
3978        declaration: &omena_cascade::CascadeDeclaration,
3979    ) -> (
3980        omena_cascade::CascadeLevel,
3981        omena_cascade::LayerRank,
3982        std::cmp::Reverse<u32>,
3983        omena_cascade::Specificity,
3984        omena_cascade::ModuleRank,
3985    ) {
3986        (
3987            declaration.key.level,
3988            declaration.key.layer_rank,
3989            std::cmp::Reverse(declaration.key.scope_proximity),
3990            declaration.key.specificity,
3991            declaration.open_world_tie_evidence.module_rank,
3992        )
3993    }
3994
3995    #[test]
3996    fn linker_distinguishes_configured_module_instances() {
3997        use omena_parser::{ConfigurationHashV0, ModuleIdV0, ModuleInstanceKeyV0};
3998
3999        let module = ModuleIdV0::new("src/theme.scss");
4000        let blue =
4001            ModuleInstanceKeyV0::new(module.clone(), ConfigurationHashV0::new("with:brand=blue"));
4002        let red = ModuleInstanceKeyV0::new(module, ConfigurationHashV0::new("with:brand=red"));
4003
4004        assert_ne!(blue, red);
4005        assert_eq!(blue.module(), red.module());
4006        assert_ne!(blue.configuration(), red.configuration());
4007    }
4008
4009    #[test]
4010    fn entrypoint_prefers_the_unconfigured_instance() -> Result<(), String> {
4011        let modules = vec![
4012            TransformBundleModuleInputV0::new(
4013                "src/theme.scss",
4014                ".theme { color: black; }",
4015                StyleDialect::Scss,
4016            ),
4017            TransformBundleModuleInputV0::new(
4018                "src/theme.scss",
4019                ".theme { color: blue; }",
4020                StyleDialect::Scss,
4021            )
4022            .with_configuration_hash(ConfigurationHashV0::new("with|5:brand=4:blue")),
4023            TransformBundleModuleInputV0::new(
4024                "src/theme.scss",
4025                ".theme { color: red; }",
4026                StyleDialect::Scss,
4027            )
4028            .with_configuration_hash(ConfigurationHashV0::new("with|5:brand=3:red")),
4029        ];
4030
4031        let linked = link_omena_transform_bundle_modules(&["src/theme.scss"], &modules)
4032            .map_err(|error| format!("unconfigured entrypoint should be selected: {error:?}"))?;
4033        assert_eq!(linked.entrypoints.len(), 1);
4034        assert_eq!(
4035            linked.entrypoints[0].configuration(),
4036            &ConfigurationHashV0::none()
4037        );
4038        Ok(())
4039    }
4040
4041    #[test]
4042    fn entrypoint_without_an_unconfigured_instance_reports_ambiguity() {
4043        let modules = vec![
4044            TransformBundleModuleInputV0::new(
4045                "src/theme.scss",
4046                ".theme { color: blue; }",
4047                StyleDialect::Scss,
4048            )
4049            .with_configuration_hash(ConfigurationHashV0::new("with|5:brand=4:blue")),
4050            TransformBundleModuleInputV0::new(
4051                "src/theme.scss",
4052                ".theme { color: red; }",
4053                StyleDialect::Scss,
4054            )
4055            .with_configuration_hash(ConfigurationHashV0::new("with|5:brand=3:red")),
4056        ];
4057
4058        assert_eq!(
4059            link_omena_transform_bundle_modules(&["src/theme.scss"], &modules),
4060            Err(TransformBundleLinkErrorV0::AmbiguousModulePath {
4061                source_path: "src/theme.scss".to_string(),
4062            })
4063        );
4064    }
4065
4066    #[test]
4067    fn semantic_reachability_input_feeds_closed_world_bundle() -> Result<(), String> {
4068        let modules = vec![TransformBundleModuleInputV0::new(
4069            "Button.module.css",
4070            ".used { color: blue; } .dead { color: red; }",
4071            StyleDialect::Css,
4072        )];
4073        let mut reachability = TransformBundleSemanticReachabilityInputV0::new("Button.module.css");
4074        reachability.class_names.push("used".to_string());
4075
4076        let linked = link_omena_transform_bundle_modules_with_semantic_reachability(
4077            &["Button.module.css"],
4078            &modules,
4079            &[reachability],
4080        )
4081        .map_err(|err| format!("semantic reachability bundle should link: {err:?}"))?;
4082
4083        assert_eq!(
4084            linked.closed_world_bundle.reachability().class_names(),
4085            &["used".to_string()]
4086        );
4087        let instance = ModuleInstanceKeyV0::unconfigured(ModuleIdV0::new("Button.module.css"));
4088        assert_eq!(
4089            linked
4090                .closed_world_bundle
4091                .module_reachability_evidence(&instance),
4092            ClosedWorldModuleReachabilityEvidenceV0::Supplied
4093        );
4094        Ok(())
4095    }
4096
4097    #[test]
4098    fn empty_semantic_reachability_records_module_input_absence_without_narrowing()
4099    -> Result<(), String> {
4100        let modules = vec![TransformBundleModuleInputV0::new(
4101            "Button.module.css",
4102            ".used { color: blue; } .dead { color: red; }",
4103            StyleDialect::Css,
4104        )];
4105        let reachability = TransformBundleSemanticReachabilityInputV0::new("Button.module.css");
4106
4107        let linked = link_omena_transform_bundle_modules_with_semantic_reachability(
4108            &["Button.module.css"],
4109            &modules,
4110            &[reachability],
4111        )
4112        .map_err(|err| format!("semantic reachability bundle should link: {err:?}"))?;
4113        let instance = ModuleInstanceKeyV0::unconfigured(ModuleIdV0::new("Button.module.css"));
4114
4115        assert_eq!(
4116            linked.closed_world_bundle.reachability().class_names(),
4117            &["dead".to_string(), "used".to_string()],
4118            "recording absent evidence must preserve the prior fail-open byte behavior"
4119        );
4120        assert_eq!(
4121            linked
4122                .closed_world_bundle
4123                .module_reachability_evidence(&instance),
4124            ClosedWorldModuleReachabilityEvidenceV0::ModuleReachabilityInputAbsent
4125        );
4126        Ok(())
4127    }
4128
4129    #[test]
4130    fn instance_reachability_keeps_configured_consumers_distinct() {
4131        let red = ModuleInstanceKeyV0::new(
4132            ModuleIdV0::new("shared.module.css"),
4133            ConfigurationHashV0::new("with:red"),
4134        );
4135        let blue = ModuleInstanceKeyV0::new(
4136            ModuleIdV0::new("shared.module.css"),
4137            ConfigurationHashV0::new("with:blue"),
4138        );
4139        let mut inputs = vec![
4140            LinkerInputV0 {
4141                source_path: "shared.module.css".to_string(),
4142                instance: red.clone(),
4143                dependency_edges: Vec::new(),
4144                class_names: vec!["alpha".to_string(), "beta".to_string()],
4145                keyframe_names: Vec::new(),
4146                value_names: Vec::new(),
4147                custom_property_names: Vec::new(),
4148                ordered_rules: Vec::new(),
4149            },
4150            LinkerInputV0 {
4151                source_path: "shared.module.css".to_string(),
4152                instance: blue.clone(),
4153                dependency_edges: Vec::new(),
4154                class_names: vec!["alpha".to_string(), "beta".to_string()],
4155                keyframe_names: Vec::new(),
4156                value_names: Vec::new(),
4157                custom_property_names: Vec::new(),
4158                ordered_rules: Vec::new(),
4159            },
4160        ];
4161        let mut red_reachability = TransformBundleInstanceReachabilityInputV0::new(
4162            red.clone(),
4163            InstanceReachabilityDerivationV0::PathUnionNoInstanceDiscriminator,
4164        );
4165        red_reachability.class_names.push("alpha".to_string());
4166        let mut blue_reachability = TransformBundleInstanceReachabilityInputV0::new(
4167            blue.clone(),
4168            InstanceReachabilityDerivationV0::PathUnionNoInstanceDiscriminator,
4169        );
4170        blue_reachability.class_names.push("beta".to_string());
4171
4172        let (evidence, _) = apply_semantic_reachability_to_linker_inputs(
4173            inputs.as_mut_slice(),
4174            &[red_reachability, blue_reachability],
4175        );
4176
4177        carrier_hygiene_assertions::assert_configured_instance_reachability(
4178            inputs[0].class_names.as_slice(),
4179            inputs[1].class_names.as_slice(),
4180            &evidence,
4181            &red,
4182            &blue,
4183        );
4184    }
4185
4186    #[test]
4187    fn instance_reachability_unions_duplicate_rows() {
4188        let instance = ModuleInstanceKeyV0::unconfigured(ModuleIdV0::new("shared.module.css"));
4189        let mut inputs = vec![LinkerInputV0 {
4190            source_path: "shared.module.css".to_string(),
4191            instance: instance.clone(),
4192            dependency_edges: Vec::new(),
4193            class_names: vec!["alpha".to_string(), "beta".to_string()],
4194            keyframe_names: Vec::new(),
4195            value_names: Vec::new(),
4196            custom_property_names: Vec::new(),
4197            ordered_rules: Vec::new(),
4198        }];
4199        let mut alpha = TransformBundleInstanceReachabilityInputV0::new(
4200            instance.clone(),
4201            InstanceReachabilityDerivationV0::PathUnionNoInstanceDiscriminator,
4202        );
4203        alpha.class_names.push("alpha".to_string());
4204        let mut beta = TransformBundleInstanceReachabilityInputV0::new(
4205            instance,
4206            InstanceReachabilityDerivationV0::PathUnionNoInstanceDiscriminator,
4207        );
4208        beta.class_names.push("beta".to_string());
4209
4210        apply_semantic_reachability_to_linker_inputs(inputs.as_mut_slice(), &[alpha, beta]);
4211
4212        carrier_hygiene_assertions::assert_duplicate_instance_reachability(
4213            inputs[0].class_names.as_slice(),
4214        );
4215    }
4216
4217    #[test]
4218    fn legacy_path_reachability_unions_normalized_rows_across_symbol_sets() {
4219        let modules = vec![TransformBundleModuleInputV0::new(
4220            "shared.module.css",
4221            r#"
4222@value primary: red;
4223@value secondary: blue;
4224@keyframes enter { from { opacity: 0; } to { opacity: 1; } }
4225@keyframes leave { from { opacity: 1; } to { opacity: 0; } }
4226:root { --primary: red; --secondary: blue; }
4227.alpha { animation: enter 1s; }
4228.beta { animation: leave 1s; }
4229"#,
4230            StyleDialect::Css,
4231        )];
4232        let mut first = TransformBundleSemanticReachabilityInputV0::new("./shared.module.css");
4233        first.class_names.push("alpha".to_string());
4234        first.keyframe_names.push("enter".to_string());
4235        first.value_names.push("primary".to_string());
4236        first.custom_property_names.push("--primary".to_string());
4237        let mut second = TransformBundleSemanticReachabilityInputV0::new("shared.module.css");
4238        second.class_names.push("beta".to_string());
4239        second.keyframe_names.push("leave".to_string());
4240        second.value_names.push("secondary".to_string());
4241        second.custom_property_names.push("--secondary".to_string());
4242
4243        let projection = project_omena_transform_bundle_linker_inputs(&modules, &[first, second]);
4244        let input = &projection.inputs()[0];
4245
4246        carrier_hygiene_assertions::assert_legacy_path_union(input);
4247    }
4248
4249    fn incomplete_composes_carrier_fixture() -> (
4250        Vec<LinkerInputV0>,
4251        BTreeMap<ModuleInstanceKeyV0, ClosedWorldModuleReachabilityEvidenceV0>,
4252        ModuleInstanceKeyV0,
4253        ModuleInstanceKeyV0,
4254    ) {
4255        let source = ModuleInstanceKeyV0::unconfigured(ModuleIdV0::new("entry.module.css"));
4256        let target = ModuleInstanceKeyV0::unconfigured(ModuleIdV0::new("base.module.css"));
4257        let mut inputs = vec![
4258            LinkerInputV0 {
4259                source_path: "entry.module.css".to_string(),
4260                instance: source.clone(),
4261                dependency_edges: vec![LinkerDependencyEdgeV0 {
4262                    kind: TransformBundleEdgeKind::CssModuleComposesExternal,
4263                    import_source: "./base.module.css".to_string(),
4264                    import_ordinal: Some(0),
4265                    local_names: Vec::new(),
4266                    remote_names: vec!["base".to_string()],
4267                }],
4268                class_names: vec!["card".to_string(), "other".to_string()],
4269                keyframe_names: Vec::new(),
4270                value_names: Vec::new(),
4271                custom_property_names: Vec::new(),
4272                ordered_rules: Vec::new(),
4273            },
4274            LinkerInputV0 {
4275                source_path: "base.module.css".to_string(),
4276                instance: target.clone(),
4277                dependency_edges: Vec::new(),
4278                class_names: vec!["base".to_string(), "other".to_string()],
4279                keyframe_names: Vec::new(),
4280                value_names: Vec::new(),
4281                custom_property_names: Vec::new(),
4282                ordered_rules: Vec::new(),
4283            },
4284        ];
4285        let mut source_reachability = TransformBundleInstanceReachabilityInputV0::new(
4286            source.clone(),
4287            InstanceReachabilityDerivationV0::PathUnionNoInstanceDiscriminator,
4288        );
4289        source_reachability.class_names.push("card".to_string());
4290        let mut target_reachability = TransformBundleInstanceReachabilityInputV0::new(
4291            target.clone(),
4292            InstanceReachabilityDerivationV0::PathUnionNoInstanceDiscriminator,
4293        );
4294        target_reachability.class_names.push("base".to_string());
4295
4296        let (evidence, _) = apply_semantic_reachability_to_linker_inputs(
4297            inputs.as_mut_slice(),
4298            &[source_reachability, target_reachability],
4299        );
4300        (inputs, evidence, source, target)
4301    }
4302
4303    #[test]
4304    fn incomplete_composes_carrier_marks_closure_target_evidence_absent() {
4305        let (_, evidence, source, target) = incomplete_composes_carrier_fixture();
4306        carrier_hygiene_assertions::assert_incomplete_composes_target_evidence(
4307            &evidence, &source, &target,
4308        );
4309    }
4310
4311    #[test]
4312    fn incomplete_composes_carrier_keeps_closure_target_symbols_fail_open() {
4313        let (inputs, _, _, _) = incomplete_composes_carrier_fixture();
4314        carrier_hygiene_assertions::assert_incomplete_composes_target_symbols(
4315            inputs[0].class_names.as_slice(),
4316            inputs[1].class_names.as_slice(),
4317        );
4318    }
4319
4320    #[test]
4321    fn external_composes_names_reach_the_sealed_closed_world_bundle() -> Result<(), String> {
4322        let modules = vec![
4323            TransformBundleModuleInputV0::new(
4324                "entry.module.css",
4325                ".card { composes: base from \"./base.module.css\"; color: red; }",
4326                StyleDialect::Css,
4327            ),
4328            TransformBundleModuleInputV0::new(
4329                "base.module.css",
4330                ".base { padding: 8px; }",
4331                StyleDialect::Css,
4332            ),
4333        ];
4334        let linked = link_omena_transform_bundle_modules(&["entry.module.css"], &modules)
4335            .map_err(|error| format!("composes fixture should link: {error:?}"))?;
4336
4337        let edges = linked.closed_world_bundle.composes_edges();
4338        assert_eq!(edges.len(), 1);
4339        assert_eq!(edges[0].from_module.module().as_str(), "entry.module.css");
4340        assert_eq!(edges[0].from_symbol, "card");
4341        assert_eq!(edges[0].to_module.module().as_str(), "base.module.css");
4342        assert_eq!(edges[0].to_symbol, "base");
4343        Ok(())
4344    }
4345
4346    #[test]
4347    fn composes_closure_expands_module_qualified_semantic_reachability() -> Result<(), String> {
4348        let modules = vec![
4349            TransformBundleModuleInputV0::new(
4350                "entry.module.css",
4351                ".card { composes: base from \"./base.module.css\"; color: red; }",
4352                StyleDialect::Css,
4353            ),
4354            TransformBundleModuleInputV0::new(
4355                "base.module.css",
4356                ".base { padding: 8px; } .other { color: green; }",
4357                StyleDialect::Css,
4358            ),
4359        ];
4360        let mut entry_reachability =
4361            TransformBundleSemanticReachabilityInputV0::new("entry.module.css");
4362        entry_reachability.class_names.push("card".to_string());
4363        let mut base_reachability =
4364            TransformBundleSemanticReachabilityInputV0::new("base.module.css");
4365        base_reachability.class_names.push("other".to_string());
4366
4367        let linked = link_omena_transform_bundle_modules_with_semantic_reachability(
4368            &["base.module.css"],
4369            &modules,
4370            &[entry_reachability, base_reachability],
4371        )
4372        .map_err(|error| format!("composes reachability fixture should link: {error:?}"))?;
4373        let entry = ModuleInstanceKeyV0::unconfigured(ModuleIdV0::new("entry.module.css"));
4374        let base = ModuleInstanceKeyV0::unconfigured(ModuleIdV0::new("base.module.css"));
4375
4376        assert_eq!(
4377            linked.closed_world_bundle.linked_modules(),
4378            std::slice::from_ref(&base),
4379            "workspace scan evidence must not widen the emission module set"
4380        );
4381        assert_eq!(
4382            linked
4383                .closed_world_bundle
4384                .reachability()
4385                .symbols_for_module(&base)
4386                .map(|symbols| symbols.class_names()),
4387            Some(&["base".to_string(), "other".to_string()][..])
4388        );
4389        assert_eq!(linked.closed_world_bundle.composes_edges().len(), 1);
4390        assert_eq!(
4391            linked
4392                .closed_world_bundle
4393                .composes_origin_symbol_is_reachable(&entry, "card"),
4394            Some(true)
4395        );
4396        Ok(())
4397    }
4398
4399    #[test]
4400    fn projection_linker_core_links_without_module_sources() -> Result<(), String> {
4401        let app = ModuleInstanceKeyV0::new(
4402            ModuleIdV0::new("src/app.module.css"),
4403            ConfigurationHashV0::none(),
4404        );
4405        let theme = ModuleInstanceKeyV0::new(
4406            ModuleIdV0::new("src/theme.css"),
4407            ConfigurationHashV0::none(),
4408        );
4409        let linked = link_stylesheet_from_projection(
4410            &["src/app.module.css"],
4411            &[
4412                LinkerInputV0 {
4413                    source_path: "src/app.module.css".to_string(),
4414                    instance: app.clone(),
4415                    dependency_edges: vec![LinkerDependencyEdgeV0 {
4416                        kind: TransformBundleEdgeKind::CssImport,
4417                        import_source: "./theme.css".to_string(),
4418                        import_ordinal: Some(0),
4419                        local_names: Vec::new(),
4420                        remote_names: Vec::new(),
4421                    }],
4422                    class_names: vec!["app".to_string()],
4423                    keyframe_names: Vec::new(),
4424                    value_names: Vec::new(),
4425                    custom_property_names: Vec::new(),
4426                    ordered_rules: vec![LinkerRuleV0 {
4427                        selector_name: "app".to_string(),
4428                        selector_kind: ParsedSelectorFactKind::Class,
4429                        range_start: 0,
4430                        range_end: 4,
4431                    }],
4432                },
4433                LinkerInputV0 {
4434                    source_path: "src/theme.css".to_string(),
4435                    instance: theme,
4436                    dependency_edges: Vec::new(),
4437                    class_names: vec!["theme".to_string()],
4438                    keyframe_names: Vec::new(),
4439                    value_names: Vec::new(),
4440                    custom_property_names: vec!["--brand".to_string()],
4441                    ordered_rules: vec![LinkerRuleV0 {
4442                        selector_name: "theme".to_string(),
4443                        selector_kind: ParsedSelectorFactKind::Class,
4444                        range_start: 0,
4445                        range_end: 6,
4446                    }],
4447                },
4448            ],
4449        )
4450        .map_err(|err| format!("{err:?}"))?;
4451
4452        assert_eq!(linked.module_instances.len(), 2);
4453        assert_eq!(
4454            linked
4455                .global_rule_order
4456                .rules
4457                .iter()
4458                .map(|rule| rule.selector_name.as_str())
4459                .collect::<Vec<_>>(),
4460            vec!["app", "theme"]
4461        );
4462        assert!(
4463            linked
4464                .closed_world_bundle
4465                .reachability()
4466                .custom_property_names()
4467                .contains(&"--brand".to_string())
4468        );
4469        Ok(())
4470    }
4471
4472    #[test]
4473    fn linker_reports_missing_module_dependency() {
4474        let modules = vec![TransformBundleModuleInputV0::new(
4475            "src/app.css",
4476            r#"@import "./missing.css"; .button { color: red; }"#,
4477            StyleDialect::Css,
4478        )];
4479
4480        let err = link_omena_transform_bundle_modules(&["src/app.css"], &modules);
4481
4482        assert_eq!(
4483            err,
4484            Err(TransformBundleLinkErrorV0::MissingDependency {
4485                source_path: "src/app.css".to_string(),
4486                import_source: "./missing.css".to_string(),
4487            })
4488        );
4489    }
4490
4491    #[test]
4492    fn resolved_dependency_carrier_links_package_export_target() -> Result<(), String> {
4493        let modules = vec![
4494            TransformBundleModuleInputV0::new(
4495                "src/app.css",
4496                r#"@import "@acme/theme/tokens.css"; .app { color: green; }"#,
4497                StyleDialect::Css,
4498            ),
4499            TransformBundleModuleInputV0::new(
4500                "node_modules/@acme/theme/dist/tokens.css",
4501                ".token { color: rebeccapurple; }",
4502                StyleDialect::Css,
4503            ),
4504        ];
4505        let projection = project_omena_transform_bundle_linker_inputs(&modules, &[]);
4506        let resolved = TransformBundleResolvedDependencyV0::new(
4507            modules[0].module_instance_key(),
4508            TransformBundleEdgeKind::CssImport,
4509            "@acme/theme/tokens.css",
4510            Some(0),
4511            TransformBundleDependencyResolutionV0::attempted(
4512                vec![
4513                    "externalUrlBoundary",
4514                    "bundlerPathMapping",
4515                    "tsconfigPathMapping",
4516                    "sassPkgImporter",
4517                    "fileRelativeOrAbsolute",
4518                    "packageManifestSubpath",
4519                    "nodePackageFallback",
4520                    "sassLoadPathRoot",
4521                ],
4522                "packageStyleModule",
4523                1,
4524                Some(modules[1].module_instance_key()),
4525            ),
4526        );
4527
4528        let linked = link_omena_transform_bundle_projection_with_resolved_dependencies_and_options(
4529            &["src/app.css"],
4530            &projection,
4531            std::slice::from_ref(&resolved),
4532            &[],
4533            TransformBundleLinkOptionsV0::default(),
4534        )
4535        .map_err(|error| format!("resolved package export should link: {error:?}"))?;
4536
4537        assert_eq!(linked.module_instances.len(), 2);
4538        assert!(
4539            linked
4540                .module_instances
4541                .contains(&modules[1].module_instance_key())
4542        );
4543        assert_eq!(resolved.resolution.attempt_state, "attempted");
4544        assert_eq!(
4545            resolved.resolution.resolution_kind,
4546            Some("packageStyleModule")
4547        );
4548        assert_eq!(resolved.resolution.policy_step_keys.len(), 8);
4549        Ok(())
4550    }
4551
4552    #[test]
4553    fn resolution_authority_is_enforced_per_dependency_edge() -> Result<(), String> {
4554        let modules = vec![
4555            TransformBundleModuleInputV0::new(
4556                "src/app.css",
4557                r#"@import "./tokens.css"; @import "./theme"; .app { color: green; }"#,
4558                StyleDialect::Css,
4559            ),
4560            TransformBundleModuleInputV0::new(
4561                "src/tokens.css",
4562                ".token { color: rebeccapurple; }",
4563                StyleDialect::Css,
4564            ),
4565            TransformBundleModuleInputV0::new(
4566                "src/theme.scss",
4567                ".theme { color: purple; }",
4568                StyleDialect::Scss,
4569            ),
4570        ];
4571        let projections =
4572            super::project_omena_transform_bundle_linker_and_emission_items(&modules, &[]);
4573        let resolved_tokens = TransformBundleResolvedDependencyV0::new(
4574            modules[0].module_instance_key(),
4575            TransformBundleEdgeKind::CssImport,
4576            "./tokens.css",
4577            Some(0),
4578            TransformBundleDependencyResolutionV0::attempted(
4579                vec!["fileRelativeOrAbsolute"],
4580                "fileRelative",
4581                1,
4582                Some(modules[1].module_instance_key()),
4583            ),
4584        );
4585        let resolved_theme = TransformBundleResolvedDependencyV0::new(
4586            modules[0].module_instance_key(),
4587            TransformBundleEdgeKind::CssImport,
4588            "./theme",
4589            Some(1),
4590            TransformBundleDependencyResolutionV0::attempted(
4591                vec!["fileRelativeOrAbsolute"],
4592                "fileRelative",
4593                1,
4594                Some(modules[2].module_instance_key()),
4595            ),
4596        );
4597
4598        let strict_error = super::link_resolved_bundle(
4599            &["src/app.css"],
4600            projections.linker_projection(),
4601            projections.emission_item_projection(),
4602            std::slice::from_ref(&resolved_tokens),
4603            &[],
4604            super::EmissionOrderingPolicyV0::ImportOrderPreserving,
4605        );
4606        let legacy = super::link_legacy_path_inferred_bundle(
4607            &["src/app.css"],
4608            projections.linker_projection(),
4609            projections.emission_item_projection(),
4610            std::slice::from_ref(&resolved_tokens),
4611            &[],
4612            super::EmissionOrderingPolicyV0::ImportOrderPreserving,
4613        )
4614        .map_err(|error| format!("legacy fallback should link: {error:?}"))?;
4615        let inferred = legacy
4616            .dependency_resolution_disclosures
4617            .iter()
4618            .filter(|disclosure| {
4619                disclosure.authority == super::BundleResolutionAuthorityV0::LegacyPathInferred
4620            })
4621            .collect::<Vec<_>>();
4622        let strict = super::link_resolved_bundle(
4623            &["src/app.css"],
4624            projections.linker_projection(),
4625            projections.emission_item_projection(),
4626            &[resolved_tokens, resolved_theme],
4627            &[],
4628            super::EmissionOrderingPolicyV0::ImportOrderPreserving,
4629        )
4630        .map_err(|error| format!("complete resolved edge set should link: {error:?}"))?;
4631        carrier_hygiene_assertions::assert_resolution_authority(
4632            &strict_error,
4633            inferred.as_slice(),
4634            strict.dependency_resolution_disclosures.as_slice(),
4635        );
4636        Ok(())
4637    }
4638
4639    #[test]
4640    fn emission_item_projection_preserves_the_legacy_selector_order() -> Result<(), String> {
4641        let modules = vec![TransformBundleModuleInputV0::new(
4642            "src/theme.css",
4643            ":root { --brand: red; }\n\
4644             @layer reset;\n\
4645             div, .theme, [hidden], *::before { color: var(--brand); }\n\
4646             @keyframes pulse { from { opacity: 0; } }",
4647            StyleDialect::Css,
4648        )];
4649        let legacy = link_omena_transform_bundle_modules(&["src/theme.css"], &modules)
4650            .map_err(|error| format!("legacy link failed: {error:?}"))?;
4651        let projections =
4652            super::project_omena_transform_bundle_linker_and_emission_items(&modules, &[]);
4653        let widened =
4654            super::link_omena_transform_bundle_projection_with_emission_items_and_resolved_dependencies_and_options(
4655                &["src/theme.css"],
4656                projections.linker_projection(),
4657                projections.emission_item_projection(),
4658                &[],
4659                &[],
4660                TransformBundleLinkOptionsV0::default(),
4661            )
4662            .map_err(|error| format!("emission-item link failed: {error:?}"))?;
4663
4664        assert_eq!(widened.linked_stylesheet, legacy);
4665        assert_eq!(
4666            widened
4667                .linked_stylesheet
4668                .global_rule_order
4669                .rules
4670                .iter()
4671                .map(|rule| (rule.global_order_index, rule.selector_name.as_str()))
4672                .collect::<Vec<_>>(),
4673            vec![(0, "theme")]
4674        );
4675        assert!(widened.emission_item_order.items.iter().any(|item| {
4676            item.kind == super::EmissionItemKindV0::SelectorPseudoClass && item.name == ":root"
4677        }));
4678        assert!(widened.emission_item_order.items.iter().any(|item| {
4679            item.kind == super::EmissionItemKindV0::KeyframesDeclaration && item.name == "pulse"
4680        }));
4681        assert!(
4682            widened
4683                .emission_item_order
4684                .items
4685                .windows(2)
4686                .all(|pair| pair[0].global_order_index + 1 == pair[1].global_order_index)
4687        );
4688        Ok(())
4689    }
4690
4691    #[test]
4692    fn emission_admission_separates_legacy_success_from_requested_projection_failure() {
4693        let modules = vec![TransformBundleModuleInputV0::new(
4694            "src/app.css",
4695            ".app { color: green; }",
4696            StyleDialect::Css,
4697        )];
4698        let projections =
4699            super::project_omena_transform_bundle_linker_and_emission_items(&modules, &[]);
4700        let empty_emission_projection =
4701            super::TransformBundleEmissionItemProjectionV0::new(Vec::new());
4702
4703        let admission =
4704            super::evaluate_omena_transform_bundle_projection_emission_admission_with_resolved_dependencies_and_options(
4705                &["src/app.css"],
4706                projections.linker_projection(),
4707                &empty_emission_projection,
4708                &[],
4709                &[],
4710                TransformBundleLinkOptionsV0::default(),
4711            );
4712
4713        assert!(!admission.module_id_legacy_open());
4714        assert!(matches!(
4715            admission.requested_policy_result(),
4716            Err(TransformBundleLinkErrorV0::InvalidEmissionPlan { reason })
4717                if reason.contains("has no emission-item input")
4718        ));
4719    }
4720
4721    #[test]
4722    fn emission_admission_marks_both_paths_open_when_preparation_fails() {
4723        let modules = vec![TransformBundleModuleInputV0::new(
4724            "src/app.css",
4725            ".app { color: green; }",
4726            StyleDialect::Css,
4727        )];
4728        let projections =
4729            super::project_omena_transform_bundle_linker_and_emission_items(&modules, &[]);
4730
4731        let admission =
4732            super::evaluate_omena_transform_bundle_projection_emission_admission_with_resolved_dependencies_and_options(
4733                &["src/missing.css"],
4734                projections.linker_projection(),
4735                projections.emission_item_projection(),
4736                &[],
4737                &[],
4738                TransformBundleLinkOptionsV0::default(),
4739            );
4740
4741        assert!(admission.module_id_legacy_open());
4742        assert!(matches!(
4743            admission.requested_policy_result(),
4744            Err(TransformBundleLinkErrorV0::MissingEntrypoint { source_path })
4745                if source_path == "src/missing.css"
4746        ));
4747    }
4748
4749    #[test]
4750    fn emission_item_projection_parses_each_module_once() -> Result<(), String> {
4751        let modules = vec![
4752            TransformBundleModuleInputV0::new(
4753                "src/reset.css",
4754                "html { box-sizing: border-box; }",
4755                StyleDialect::Css,
4756            ),
4757            TransformBundleModuleInputV0::new(
4758                "src/theme.css",
4759                ":root { --brand: red; }",
4760                StyleDialect::Css,
4761            ),
4762            TransformBundleModuleInputV0::new(
4763                "src/app.css",
4764                ".app { color: var(--brand); }",
4765                StyleDialect::Css,
4766            ),
4767        ];
4768        let (linked, parser_snapshot) = omena_parser::with_omena_parser_parse_instrumentation(
4769            || {
4770                let projections =
4771                    super::project_omena_transform_bundle_linker_and_emission_items(&modules, &[]);
4772                super::link_omena_transform_bundle_projection_with_emission_items_and_resolved_dependencies_and_options(
4773                    &["src/reset.css", "src/theme.css", "src/app.css"],
4774                    projections.linker_projection(),
4775                    projections.emission_item_projection(),
4776                    &[],
4777                    &[],
4778                    TransformBundleLinkOptionsV0::default(),
4779                )
4780            },
4781        );
4782        let linked = linked.map_err(|error| format!("emission-item link failed: {error:?}"))?;
4783
4784        assert_eq!(parser_snapshot.parse_invocation_count, 3);
4785        assert_eq!(linked.linked_stylesheet.module_instances.len(), 3);
4786        Ok(())
4787    }
4788
4789    fn materialize_with_emission_items(
4790        entrypoint: &str,
4791        modules: &[TransformBundleModuleInputV0],
4792        transformed_css: &[(&str, &str)],
4793    ) -> Result<
4794        (
4795            super::LinkedStylesheetWithEmissionItemsV0,
4796            super::LinkedEmissionArtifactV0,
4797        ),
4798        String,
4799    > {
4800        let projections =
4801            super::project_omena_transform_bundle_linker_and_emission_items(modules, &[]);
4802        let linked =
4803            super::link_omena_transform_bundle_projection_with_emission_items_and_resolved_dependencies_and_options(
4804                &[entrypoint],
4805                projections.linker_projection(),
4806                projections.emission_item_projection(),
4807                &[],
4808                &[],
4809                TransformBundleLinkOptionsV0 {
4810                    emission_ordering_policy:
4811                        super::EmissionOrderingPolicyV0::ImportOrderPreserving,
4812                    ..TransformBundleLinkOptionsV0::default()
4813                },
4814            )
4815            .map_err(|error| format!("emission-item link failed: {error:?}"))?;
4816        let transformed_modules = transformed_css
4817            .iter()
4818            .map(|(source_path, css)| {
4819                let module = modules
4820                    .iter()
4821                    .find(|module| module.source_path == *source_path)
4822                    .ok_or_else(|| format!("missing module input for {source_path}"))?;
4823                Ok(TransformBundleTransformedModuleV0::new(
4824                    module.module_instance_key(),
4825                    *css,
4826                ))
4827            })
4828            .collect::<Result<Vec<_>, String>>()?;
4829        let artifact =
4830            super::materialize_omena_transform_bundle_linked_stylesheet_with_emission_items(
4831                &linked,
4832                &transformed_modules,
4833            )
4834            .map_err(|error| format!("emission-item materialization failed: {error:?}"))?;
4835        Ok((linked, artifact))
4836    }
4837
4838    #[test]
4839    fn emission_items_place_element_only_import_before_the_importer() -> Result<(), String> {
4840        let modules = [
4841            TransformBundleModuleInputV0::new(
4842                "src/app.css",
4843                "@import \"./reset.css\"; div { color: red; }",
4844                StyleDialect::Css,
4845            ),
4846            TransformBundleModuleInputV0::new(
4847                "src/reset.css",
4848                "div { color: green; }",
4849                StyleDialect::Css,
4850            ),
4851        ];
4852        let (linked, artifact) = materialize_with_emission_items(
4853            "src/app.css",
4854            &modules,
4855            &[
4856                ("src/app.css", "div { color: red; }"),
4857                ("src/reset.css", "div { color: green; }"),
4858            ],
4859        )?;
4860
4861        let mut seen_modules = BTreeSet::new();
4862        let first_module_occurrences = linked
4863            .emission_item_order
4864            .items
4865            .iter()
4866            .filter_map(|item| {
4867                seen_modules
4868                    .insert(item.module_instance.clone())
4869                    .then_some(item.module_instance.module().as_str())
4870            })
4871            .collect::<Vec<_>>();
4872        assert_eq!(
4873            first_module_occurrences,
4874            vec!["src/reset.css", "src/app.css"]
4875        );
4876        assert_eq!(
4877            artifact.output_css,
4878            "div { color: green; }\ndiv { color: red; }"
4879        );
4880        Ok(())
4881    }
4882
4883    #[test]
4884    fn emission_items_do_not_relocate_an_element_rule_past_named_rules() -> Result<(), String> {
4885        let modules = [
4886            TransformBundleModuleInputV0::new(
4887                "src/app.css",
4888                "@import \"./reset.css\"; .card { padding: 1px; } div { color: red; }",
4889                StyleDialect::Css,
4890            ),
4891            TransformBundleModuleInputV0::new(
4892                "src/reset.css",
4893                "div { color: green; }",
4894                StyleDialect::Css,
4895            ),
4896        ];
4897        let (_, artifact) = materialize_with_emission_items(
4898            "src/app.css",
4899            &modules,
4900            &[
4901                ("src/app.css", ".card { padding: 1px; } div { color: red; }"),
4902                ("src/reset.css", "div { color: green; }"),
4903            ],
4904        )?;
4905
4906        let green = artifact
4907            .output_css
4908            .find("green")
4909            .ok_or_else(|| "missing imported declaration".to_string())?;
4910        let card = artifact
4911            .output_css
4912            .find(".card")
4913            .ok_or_else(|| "missing named selector declaration".to_string())?;
4914        let red = artifact
4915            .output_css
4916            .find("red")
4917            .ok_or_else(|| "missing importing declaration".to_string())?;
4918        assert!(green < card && card < red);
4919        Ok(())
4920    }
4921
4922    #[test]
4923    fn emission_item_placement_is_independent_of_module_names() -> Result<(), String> {
4924        let modules = [
4925            TransformBundleModuleInputV0::new(
4926                "src/zzz-app.css",
4927                "@import \"./aaa-reset.css\"; div { color: red; }",
4928                StyleDialect::Css,
4929            ),
4930            TransformBundleModuleInputV0::new(
4931                "src/aaa-reset.css",
4932                "div { color: green; }",
4933                StyleDialect::Css,
4934            ),
4935        ];
4936        let (_, artifact) = materialize_with_emission_items(
4937            "src/zzz-app.css",
4938            &modules,
4939            &[
4940                ("src/zzz-app.css", "div { color: red; }"),
4941                ("src/aaa-reset.css", "div { color: green; }"),
4942            ],
4943        )?;
4944
4945        assert_eq!(
4946            artifact.output_css,
4947            "div { color: green; }\ndiv { color: red; }"
4948        );
4949        Ok(())
4950    }
4951
4952    #[test]
4953    fn emission_items_preserve_cascade_layer_declaration_order() -> Result<(), String> {
4954        let modules = [
4955            TransformBundleModuleInputV0::new(
4956                "src/app.css",
4957                "@import \"./layers.css\"; @layer theme { .card { color: blue; } } \
4958                 @layer base { .card { color: orange; } }",
4959                StyleDialect::Css,
4960            ),
4961            TransformBundleModuleInputV0::new(
4962                "src/layers.css",
4963                "@layer base, theme;",
4964                StyleDialect::Css,
4965            ),
4966        ];
4967        let (_, artifact) = materialize_with_emission_items(
4968            "src/app.css",
4969            &modules,
4970            &[
4971                (
4972                    "src/app.css",
4973                    "@layer theme { .card { color: blue; } } \
4974                     @layer base { .card { color: orange; } }",
4975                ),
4976                ("src/layers.css", "@layer base, theme;"),
4977            ],
4978        )?;
4979
4980        assert!(artifact.output_css.starts_with("@layer base, theme;"));
4981        assert!(
4982            artifact
4983                .output_css
4984                .find("@layer base, theme;")
4985                .unwrap_or(usize::MAX)
4986                < artifact.output_css.find("@layer theme").unwrap_or_default()
4987        );
4988        Ok(())
4989    }
4990
4991    #[test]
4992    fn emission_item_materializer_preserves_empty_module_placement() -> Result<(), String> {
4993        let modules = [
4994            TransformBundleModuleInputV0::new(
4995                "src/app.css",
4996                "@import \"./empty.css\"; @import \"./license.css\"; .app { color: red; }",
4997                StyleDialect::Css,
4998            ),
4999            TransformBundleModuleInputV0::new("src/empty.css", "", StyleDialect::Css),
5000            TransformBundleModuleInputV0::new(
5001                "src/license.css",
5002                "/* license */",
5003                StyleDialect::Css,
5004            ),
5005        ];
5006        let projections =
5007            super::project_omena_transform_bundle_linker_and_emission_items(&modules, &[]);
5008        let linked =
5009            super::link_omena_transform_bundle_projection_with_emission_items_and_resolved_dependencies_and_options(
5010                &["src/app.css"],
5011                projections.linker_projection(),
5012                projections.emission_item_projection(),
5013                &[],
5014                &[],
5015                TransformBundleLinkOptionsV0 {
5016                    emission_ordering_policy:
5017                        super::EmissionOrderingPolicyV0::ImportOrderPreserving,
5018                    ..TransformBundleLinkOptionsV0::default()
5019                },
5020            )
5021            .map_err(|error| format!("emission-item link failed: {error:?}"))?;
5022        let transformed = modules
5023            .iter()
5024            .map(|module| {
5025                let output_css = match module.source_path.as_str() {
5026                    "src/app.css" => ".app { color: red; }",
5027                    "src/license.css" => "/* license */",
5028                    _ => "",
5029                };
5030                TransformBundleTransformedModuleV0::new(
5031                    module.module_instance_key(),
5032                    output_css.to_string(),
5033                )
5034            })
5035            .collect::<Vec<_>>();
5036
5037        let artifact =
5038            super::materialize_omena_transform_bundle_linked_stylesheet_with_emission_items(
5039                &linked,
5040                &transformed,
5041            )
5042            .map_err(|error| format!("emission-item materialization failed: {error:?}"))?;
5043        assert_eq!(artifact.emitted_module_count, 3);
5044        assert_eq!(
5045            artifact
5046                .module_regions
5047                .iter()
5048                .map(|region| region.module_instance.module().as_str())
5049                .collect::<Vec<_>>(),
5050            ["src/empty.css", "src/license.css", "src/app.css"]
5051        );
5052        assert_eq!(artifact.output_css, "/* license */\n.app { color: red; }");
5053        Ok(())
5054    }
5055
5056    #[test]
5057    fn emission_item_materializer_rejects_incomplete_module_coverage() -> Result<(), String> {
5058        let modules = [
5059            TransformBundleModuleInputV0::new(
5060                "src/app.css",
5061                "@import \"./reset.css\"; .app { color: red; }",
5062                StyleDialect::Css,
5063            ),
5064            TransformBundleModuleInputV0::new(
5065                "src/reset.css",
5066                "html { box-sizing: border-box; }",
5067                StyleDialect::Css,
5068            ),
5069        ];
5070        let projections =
5071            super::project_omena_transform_bundle_linker_and_emission_items(&modules, &[]);
5072        let mut linked =
5073            super::link_omena_transform_bundle_projection_with_emission_items_and_resolved_dependencies_and_options(
5074                &["src/app.css"],
5075                projections.linker_projection(),
5076                projections.emission_item_projection(),
5077                &[],
5078                &[],
5079                TransformBundleLinkOptionsV0 {
5080                    emission_ordering_policy:
5081                        super::EmissionOrderingPolicyV0::ImportOrderPreserving,
5082                    ..TransformBundleLinkOptionsV0::default()
5083                },
5084            )
5085            .map_err(|error| format!("emission-item link failed: {error:?}"))?;
5086        let missing_module = modules[1].module_instance_key();
5087        linked
5088            .emission_item_order
5089            .items
5090            .retain(|item| item.module_instance != missing_module);
5091        for (index, item) in linked.emission_item_order.items.iter_mut().enumerate() {
5092            item.global_order_index = u32::try_from(index)
5093                .map_err(|_| "emission-item test order exceeds u32".to_string())?;
5094        }
5095        let transformed = modules
5096            .iter()
5097            .map(|module| {
5098                TransformBundleTransformedModuleV0::new(
5099                    module.module_instance_key(),
5100                    module.source.clone(),
5101                )
5102            })
5103            .collect::<Vec<_>>();
5104
5105        match super::materialize_omena_transform_bundle_linked_stylesheet_with_emission_items(
5106            &linked,
5107            &transformed,
5108        ) {
5109            Err(super::LinkedEmissionItemMaterializationErrorV0::MissingEmissionItem {
5110                module_instance,
5111            }) if module_instance == missing_module => Ok(()),
5112            result => Err(format!(
5113                "incomplete emission-item coverage returned an unexpected result: {result:?}"
5114            )),
5115        }
5116    }
5117}