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