Skip to main content

omena_semantic/
css_modules_cross_file.rs

1//! CSS Modules cross-file closure and resolution summaries for semantic consumers.
2
3use std::collections::{BTreeMap, BTreeSet};
4
5use omena_cross_file_summary::{
6    HypergraphClosurePath, collect_hypergraph_transitive_closure_paths,
7};
8use omena_resolver::{
9    OmenaResolverStylePackageManifestV0, resolve_omena_resolver_style_module_source,
10};
11use serde::Serialize;
12
13#[derive(Debug, Clone, PartialEq, Eq, Serialize)]
14#[serde(rename_all = "camelCase")]
15pub struct CssModulesCrossFileStyleFactsV0 {
16    pub style_path: String,
17    pub class_selector_names: Vec<String>,
18    pub css_module_value_definition_names: Vec<String>,
19    pub css_module_value_import_edges: Vec<CssModulesValueImportEdgeFactV0>,
20    pub css_module_value_definition_edges: Vec<CssModulesValueDefinitionEdgeFactV0>,
21    pub css_module_composes_edges: Vec<CssModulesComposesEdgeFactV0>,
22    pub icss_export_names: Vec<String>,
23    pub icss_import_edges: Vec<CssModulesIcssImportEdgeFactV0>,
24    pub icss_export_edges: Vec<CssModulesIcssExportEdgeFactV0>,
25}
26
27#[derive(Debug, Clone, PartialEq, Eq, Serialize)]
28#[serde(rename_all = "camelCase")]
29pub struct CssModulesValueImportEdgeFactV0 {
30    pub remote_name: String,
31    pub local_name: String,
32    pub import_source: String,
33}
34
35#[derive(Debug, Clone, PartialEq, Eq, Serialize)]
36#[serde(rename_all = "camelCase")]
37pub struct CssModulesValueDefinitionEdgeFactV0 {
38    pub definition_name: String,
39    pub reference_names: Vec<String>,
40}
41
42#[derive(Debug, Clone, PartialEq, Eq, Serialize)]
43#[serde(rename_all = "camelCase")]
44pub struct CssModulesComposesEdgeFactV0 {
45    pub kind: &'static str,
46    pub owner_selector_names: Vec<String>,
47    pub target_names: Vec<String>,
48    pub import_source: Option<String>,
49}
50
51#[derive(Debug, Clone, PartialEq, Eq, Serialize)]
52#[serde(rename_all = "camelCase")]
53pub struct CssModulesIcssImportEdgeFactV0 {
54    pub local_name: String,
55    pub remote_name: String,
56    pub import_source: String,
57}
58
59#[derive(Debug, Clone, PartialEq, Eq, Serialize)]
60#[serde(rename_all = "camelCase")]
61pub struct CssModulesIcssExportEdgeFactV0 {
62    pub export_name: String,
63    pub reference_names: Vec<String>,
64}
65
66#[derive(Debug, Clone, PartialEq, Eq, Serialize)]
67#[serde(rename_all = "camelCase")]
68pub struct CssModulesCrossFileClosureSummaryV0 {
69    pub schema_version: &'static str,
70    pub product: &'static str,
71    pub status: &'static str,
72    pub style_count: usize,
73    pub composes_closure_edge_count: usize,
74    pub value_closure_edge_count: usize,
75    pub icss_closure_edge_count: usize,
76    pub composes_cycle_count: usize,
77    pub value_cycle_count: usize,
78    pub icss_cycle_count: usize,
79    pub composes_closure_edges: Vec<CssModulesComposesClosureEdgeV0>,
80    pub value_closure_edges: Vec<CssModulesValueClosureEdgeV0>,
81    pub icss_closure_edges: Vec<CssModulesIcssClosureEdgeV0>,
82    pub cycles: Vec<CssModulesCycleV0>,
83    pub capabilities: CssModulesCrossFileClosureCapabilitiesV0,
84}
85
86#[derive(Debug, Clone, PartialEq, Eq, Serialize)]
87#[serde(rename_all = "camelCase")]
88pub struct CssModulesCrossFileClosureCapabilitiesV0 {
89    pub semantic_layer_owned: bool,
90    pub composes_closure_ready: bool,
91    pub value_graph_closure_ready: bool,
92    pub icss_export_import_closure_ready: bool,
93    pub cycle_detection_ready: bool,
94}
95
96#[derive(Debug, Clone, PartialEq, Eq, Serialize)]
97#[serde(rename_all = "camelCase")]
98pub struct CssModulesCrossFileResolutionSummaryV0 {
99    pub schema_version: &'static str,
100    pub product: &'static str,
101    pub status: &'static str,
102    pub resolution_scope: &'static str,
103    pub style_count: usize,
104    pub import_edge_count: usize,
105    pub resolved_import_edge_count: usize,
106    pub unresolved_import_edge_count: usize,
107    pub matched_name_count: usize,
108    pub edges: Vec<CssModulesImportEdgeResolutionV0>,
109    pub composes_closure_edge_count: usize,
110    pub value_closure_edge_count: usize,
111    pub icss_closure_edge_count: usize,
112    pub composes_cycle_count: usize,
113    pub value_cycle_count: usize,
114    pub icss_cycle_count: usize,
115    pub composes_closure_edges: Vec<CssModulesComposesClosureEdgeV0>,
116    pub value_closure_edges: Vec<CssModulesValueClosureEdgeV0>,
117    pub icss_closure_edges: Vec<CssModulesIcssClosureEdgeV0>,
118    pub cycles: Vec<CssModulesCycleV0>,
119    pub capabilities: CssModulesCrossFileResolutionCapabilitiesV0,
120}
121
122#[derive(Debug, Clone, PartialEq, Eq, Serialize)]
123#[serde(rename_all = "camelCase")]
124pub struct CssModulesImportEdgeResolutionV0 {
125    pub from_style_path: String,
126    pub import_kind: &'static str,
127    pub source: String,
128    pub resolved_style_path: Option<String>,
129    pub status: &'static str,
130    pub import_graph_distance: Option<usize>,
131    pub import_graph_order: Option<usize>,
132    pub imported_names: Vec<String>,
133    pub exported_names: Vec<String>,
134    pub matched_names: Vec<String>,
135}
136
137#[derive(Debug, Clone, PartialEq, Eq, Serialize)]
138#[serde(rename_all = "camelCase")]
139pub struct CssModulesCrossFileResolutionCapabilitiesV0 {
140    pub semantic_layer_owned: bool,
141    pub import_source_resolution_ready: bool,
142    pub composes_name_match_ready: bool,
143    pub value_name_match_ready: bool,
144    pub icss_name_match_ready: bool,
145    pub transitive_closure_ready: bool,
146    pub value_graph_closure_ready: bool,
147    pub icss_export_import_closure_ready: bool,
148    pub cycle_detection_ready: bool,
149}
150
151#[derive(Debug, Clone, PartialEq, Eq, Serialize)]
152#[serde(rename_all = "camelCase")]
153pub struct CssModulesComposesClosureEdgeV0 {
154    pub from_style_path: String,
155    pub owner_selector_name: String,
156    pub target_style_path: String,
157    pub target_selector_name: String,
158    pub depth: usize,
159    pub path: Vec<String>,
160}
161
162#[derive(Debug, Clone, PartialEq, Eq, Serialize)]
163#[serde(rename_all = "camelCase")]
164pub struct CssModulesValueClosureEdgeV0 {
165    pub from_style_path: String,
166    pub value_name: String,
167    pub target_style_path: String,
168    pub target_value_name: String,
169    pub depth: usize,
170    pub path: Vec<String>,
171}
172
173#[derive(Debug, Clone, PartialEq, Eq, Serialize)]
174#[serde(rename_all = "camelCase")]
175pub struct CssModulesIcssClosureEdgeV0 {
176    pub from_style_path: String,
177    pub name: String,
178    pub target_style_path: String,
179    pub target_name: String,
180    pub depth: usize,
181    pub path: Vec<String>,
182}
183
184#[derive(Debug, Clone, PartialEq, Eq, Serialize)]
185#[serde(rename_all = "camelCase")]
186pub struct CssModulesCycleV0 {
187    pub kind: &'static str,
188    pub path: Vec<String>,
189}
190
191pub fn summarize_css_modules_cross_file_closure(
192    style_facts: &[CssModulesCrossFileStyleFactsV0],
193    package_manifests: &[OmenaResolverStylePackageManifestV0],
194) -> CssModulesCrossFileClosureSummaryV0 {
195    let available_style_paths = style_facts
196        .iter()
197        .map(|entry| entry.style_path.as_str())
198        .collect::<BTreeSet<_>>();
199    let facts_by_path = style_facts
200        .iter()
201        .map(|entry| (entry.style_path.as_str(), entry))
202        .collect::<BTreeMap<_, _>>();
203    let (composes_closure_edges, cycles) = summarize_css_modules_composes_closure(
204        &facts_by_path,
205        &available_style_paths,
206        package_manifests,
207    );
208    let (value_closure_edges, value_cycles) = summarize_css_modules_value_closure(
209        &facts_by_path,
210        &available_style_paths,
211        package_manifests,
212    );
213    let (icss_closure_edges, icss_cycles) = summarize_css_modules_icss_closure(
214        &facts_by_path,
215        &available_style_paths,
216        package_manifests,
217    );
218    let composes_cycle_count = cycles.len();
219    let value_cycle_count = value_cycles.len();
220    let icss_cycle_count = icss_cycles.len();
221    let mut cycles = cycles;
222    cycles.extend(value_cycles);
223    cycles.extend(icss_cycles);
224    cycles.sort_by_key(|cycle| (cycle.kind, cycle.path.clone()));
225
226    CssModulesCrossFileClosureSummaryV0 {
227        schema_version: "0",
228        product: "omena-semantic.css-modules-cross-file-closure",
229        status: "semanticLayerOwnedClosure",
230        style_count: style_facts.len(),
231        composes_closure_edge_count: composes_closure_edges.len(),
232        value_closure_edge_count: value_closure_edges.len(),
233        icss_closure_edge_count: icss_closure_edges.len(),
234        composes_cycle_count,
235        value_cycle_count,
236        icss_cycle_count,
237        composes_closure_edges,
238        value_closure_edges,
239        icss_closure_edges,
240        cycles,
241        capabilities: CssModulesCrossFileClosureCapabilitiesV0 {
242            semantic_layer_owned: true,
243            composes_closure_ready: true,
244            value_graph_closure_ready: true,
245            icss_export_import_closure_ready: true,
246            cycle_detection_ready: true,
247        },
248    }
249}
250
251pub fn summarize_css_modules_cross_file_resolution(
252    style_facts: &[CssModulesCrossFileStyleFactsV0],
253    style_import_edges: &[crate::sass_module_graph::StyleImportReachabilityEdgeFactV0],
254    package_manifests: &[OmenaResolverStylePackageManifestV0],
255) -> CssModulesCrossFileResolutionSummaryV0 {
256    let available_style_paths = style_facts
257        .iter()
258        .map(|entry| entry.style_path.as_str())
259        .collect::<BTreeSet<_>>();
260    let facts_by_path = style_facts
261        .iter()
262        .map(|entry| (entry.style_path.as_str(), entry))
263        .collect::<BTreeMap<_, _>>();
264    let mut edges = Vec::new();
265
266    for entry in style_facts {
267        let style_path = entry.style_path.as_str();
268        let reachable =
269            collect_import_reachable_style_path_metadata(style_path, style_import_edges);
270
271        for edge in &entry.css_module_composes_edges {
272            let Some(source) = edge.import_source.as_deref() else {
273                continue;
274            };
275            edges.push(resolve_css_modules_import_edge(
276                style_path,
277                "composes",
278                source,
279                edge.target_names.as_slice(),
280                &available_style_paths,
281                &facts_by_path,
282                &reachable,
283                package_manifests,
284                |target| target.class_selector_names.as_slice(),
285            ));
286        }
287
288        for edge in &entry.css_module_value_import_edges {
289            edges.push(resolve_css_modules_import_edge(
290                style_path,
291                "value",
292                edge.import_source.as_str(),
293                std::slice::from_ref(&edge.remote_name),
294                &available_style_paths,
295                &facts_by_path,
296                &reachable,
297                package_manifests,
298                |target| target.css_module_value_definition_names.as_slice(),
299            ));
300        }
301
302        for edge in &entry.icss_import_edges {
303            edges.push(resolve_css_modules_import_edge(
304                style_path,
305                "icss",
306                edge.import_source.as_str(),
307                std::slice::from_ref(&edge.remote_name),
308                &available_style_paths,
309                &facts_by_path,
310                &reachable,
311                package_manifests,
312                |target| target.icss_export_names.as_slice(),
313            ));
314        }
315    }
316
317    edges.sort_by_key(|edge| {
318        (
319            edge.from_style_path.clone(),
320            edge.import_kind,
321            edge.source.clone(),
322        )
323    });
324    let closure_summary = summarize_css_modules_cross_file_closure(style_facts, package_manifests);
325    let resolved_import_edge_count = edges
326        .iter()
327        .filter(|edge| edge.resolved_style_path.is_some())
328        .count();
329    let matched_name_count = edges
330        .iter()
331        .map(|edge| edge.matched_names.len())
332        .sum::<usize>();
333
334    CssModulesCrossFileResolutionSummaryV0 {
335        schema_version: "0",
336        product: "omena-semantic.css-modules-cross-file-resolution",
337        status: "semanticLayerOwnedResolution",
338        resolution_scope: "batchImportGraph",
339        style_count: style_facts.len(),
340        import_edge_count: edges.len(),
341        resolved_import_edge_count,
342        unresolved_import_edge_count: edges.len() - resolved_import_edge_count,
343        matched_name_count,
344        edges,
345        composes_closure_edge_count: closure_summary.composes_closure_edge_count,
346        value_closure_edge_count: closure_summary.value_closure_edge_count,
347        icss_closure_edge_count: closure_summary.icss_closure_edge_count,
348        composes_cycle_count: closure_summary.composes_cycle_count,
349        value_cycle_count: closure_summary.value_cycle_count,
350        icss_cycle_count: closure_summary.icss_cycle_count,
351        composes_closure_edges: closure_summary.composes_closure_edges,
352        value_closure_edges: closure_summary.value_closure_edges,
353        icss_closure_edges: closure_summary.icss_closure_edges,
354        cycles: closure_summary.cycles,
355        capabilities: CssModulesCrossFileResolutionCapabilitiesV0 {
356            semantic_layer_owned: true,
357            import_source_resolution_ready: true,
358            composes_name_match_ready: true,
359            value_name_match_ready: true,
360            icss_name_match_ready: true,
361            transitive_closure_ready: true,
362            value_graph_closure_ready: true,
363            icss_export_import_closure_ready: true,
364            cycle_detection_ready: true,
365        },
366    }
367}
368
369#[derive(Debug, Clone, Copy, PartialEq, Eq)]
370struct ImportReachability {
371    distance: usize,
372    order: usize,
373}
374
375fn collect_import_reachable_style_path_metadata(
376    target_style_path: &str,
377    style_import_edges: &[crate::sass_module_graph::StyleImportReachabilityEdgeFactV0],
378) -> BTreeMap<String, ImportReachability> {
379    crate::sass_module_graph::summarize_style_import_reachability(
380        target_style_path,
381        style_import_edges,
382    )
383    .reachable_style_paths
384    .into_iter()
385    .map(|fact| {
386        (
387            fact.style_path,
388            ImportReachability {
389                distance: fact.distance,
390                order: fact.order,
391            },
392        )
393    })
394    .collect()
395}
396
397#[allow(clippy::too_many_arguments)]
398fn resolve_css_modules_import_edge(
399    from_style_path: &str,
400    import_kind: &'static str,
401    source: &str,
402    imported_names: &[String],
403    available_style_paths: &BTreeSet<&str>,
404    facts_by_path: &BTreeMap<&str, &CssModulesCrossFileStyleFactsV0>,
405    reachable: &BTreeMap<String, ImportReachability>,
406    package_manifests: &[OmenaResolverStylePackageManifestV0],
407    exported_names_for_kind: fn(&CssModulesCrossFileStyleFactsV0) -> &[String],
408) -> CssModulesImportEdgeResolutionV0 {
409    let resolved_style_path = resolve_omena_resolver_style_module_source(
410        from_style_path,
411        source,
412        available_style_paths,
413        package_manifests,
414    );
415    let reachability = resolved_style_path
416        .as_ref()
417        .and_then(|style_path| reachable.get(style_path));
418    let exported_names = resolved_style_path
419        .as_deref()
420        .and_then(|style_path| facts_by_path.get(style_path))
421        .map(|facts| exported_names_for_kind(facts).to_vec())
422        .unwrap_or_default();
423    let imported_names = sorted_unique_strings(imported_names);
424    let matched_names =
425        sorted_name_intersection(imported_names.as_slice(), exported_names.as_slice());
426    let status = if resolved_style_path.is_none() {
427        "unresolvedSource"
428    } else if imported_names.is_empty() {
429        "resolvedSource"
430    } else if matched_names.is_empty() {
431        "resolvedSourceNoNameMatch"
432    } else {
433        "resolved"
434    };
435
436    CssModulesImportEdgeResolutionV0 {
437        from_style_path: from_style_path.to_string(),
438        import_kind,
439        source: source.to_string(),
440        resolved_style_path,
441        status,
442        import_graph_distance: reachability.map(|reachability| reachability.distance),
443        import_graph_order: reachability.map(|reachability| reachability.order),
444        imported_names,
445        exported_names,
446        matched_names,
447    }
448}
449
450fn sorted_unique_strings(values: &[String]) -> Vec<String> {
451    values
452        .iter()
453        .cloned()
454        .collect::<BTreeSet<_>>()
455        .into_iter()
456        .collect()
457}
458
459fn sorted_name_intersection(left: &[String], right: &[String]) -> Vec<String> {
460    let right = right.iter().map(String::as_str).collect::<BTreeSet<_>>();
461    left.iter()
462        .filter(|name| right.contains(name.as_str()))
463        .cloned()
464        .collect::<BTreeSet<_>>()
465        .into_iter()
466        .collect()
467}
468
469#[derive(Debug, Clone, PartialEq, Eq, PartialOrd, Ord)]
470struct CssModulesComposesNode {
471    style_path: String,
472    selector_name: String,
473}
474
475fn summarize_css_modules_composes_closure(
476    facts_by_path: &BTreeMap<&str, &CssModulesCrossFileStyleFactsV0>,
477    available_style_paths: &BTreeSet<&str>,
478    package_manifests: &[OmenaResolverStylePackageManifestV0],
479) -> (Vec<CssModulesComposesClosureEdgeV0>, Vec<CssModulesCycleV0>) {
480    let graph = collect_css_modules_composes_adjacency(
481        facts_by_path,
482        available_style_paths,
483        package_manifests,
484    );
485    let (closure_paths, cycle_paths) =
486        collect_hypergraph_transitive_closure_paths(&graph, css_modules_composes_node_label);
487    let mut closure_edges = closure_paths
488        .into_iter()
489        .map(
490            |HypergraphClosurePath {
491                 origin,
492                 target,
493                 depth,
494                 path_labels,
495             }| CssModulesComposesClosureEdgeV0 {
496                from_style_path: origin.style_path,
497                owner_selector_name: origin.selector_name,
498                target_style_path: target.style_path,
499                target_selector_name: target.selector_name,
500                depth,
501                path: path_labels,
502            },
503        )
504        .collect::<Vec<_>>();
505    let mut cycles = cycle_paths
506        .into_iter()
507        .map(|path| CssModulesCycleV0 {
508            kind: "composes",
509            path,
510        })
511        .collect::<Vec<_>>();
512
513    closure_edges.sort_by_key(|edge| {
514        (
515            edge.from_style_path.clone(),
516            edge.owner_selector_name.clone(),
517            edge.depth,
518            edge.target_style_path.clone(),
519            edge.target_selector_name.clone(),
520        )
521    });
522    cycles.sort_by_key(|cycle| cycle.path.clone());
523    (closure_edges, cycles)
524}
525
526fn collect_css_modules_composes_adjacency(
527    facts_by_path: &BTreeMap<&str, &CssModulesCrossFileStyleFactsV0>,
528    available_style_paths: &BTreeSet<&str>,
529    package_manifests: &[OmenaResolverStylePackageManifestV0],
530) -> BTreeMap<CssModulesComposesNode, BTreeSet<CssModulesComposesNode>> {
531    let mut graph = BTreeMap::new();
532    for (style_path, facts) in facts_by_path {
533        let class_names = facts
534            .class_selector_names
535            .iter()
536            .map(String::as_str)
537            .collect::<BTreeSet<_>>();
538        for edge in &facts.css_module_composes_edges {
539            if edge.kind == "global" {
540                continue;
541            }
542            let target_style_path = if edge.kind == "external" {
543                edge.import_source.as_deref().and_then(|source| {
544                    resolve_omena_resolver_style_module_source(
545                        style_path,
546                        source,
547                        available_style_paths,
548                        package_manifests,
549                    )
550                })
551            } else {
552                Some((*style_path).to_string())
553            };
554            let Some(target_style_path) = target_style_path else {
555                continue;
556            };
557            let target_class_names = if target_style_path == *style_path {
558                class_names.clone()
559            } else {
560                facts_by_path
561                    .get(target_style_path.as_str())
562                    .map(|facts| {
563                        facts
564                            .class_selector_names
565                            .iter()
566                            .map(String::as_str)
567                            .collect::<BTreeSet<_>>()
568                    })
569                    .unwrap_or_default()
570            };
571            for owner_selector_name in &edge.owner_selector_names {
572                if !class_names.contains(owner_selector_name.as_str()) {
573                    continue;
574                }
575                let owner = CssModulesComposesNode {
576                    style_path: (*style_path).to_string(),
577                    selector_name: owner_selector_name.clone(),
578                };
579                for target_selector_name in &edge.target_names {
580                    if !target_class_names.contains(target_selector_name.as_str()) {
581                        continue;
582                    }
583                    graph
584                        .entry(owner.clone())
585                        .or_insert_with(BTreeSet::new)
586                        .insert(CssModulesComposesNode {
587                            style_path: target_style_path.clone(),
588                            selector_name: target_selector_name.clone(),
589                        });
590                }
591            }
592        }
593    }
594    graph
595}
596
597fn css_modules_composes_node_label(node: &CssModulesComposesNode) -> String {
598    format!("{}#{}", node.style_path, node.selector_name)
599}
600
601#[derive(Debug, Clone, PartialEq, Eq, PartialOrd, Ord)]
602struct CssModulesValueNode {
603    style_path: String,
604    value_name: String,
605}
606
607fn summarize_css_modules_value_closure(
608    facts_by_path: &BTreeMap<&str, &CssModulesCrossFileStyleFactsV0>,
609    available_style_paths: &BTreeSet<&str>,
610    package_manifests: &[OmenaResolverStylePackageManifestV0],
611) -> (Vec<CssModulesValueClosureEdgeV0>, Vec<CssModulesCycleV0>) {
612    let graph = collect_css_modules_value_adjacency(
613        facts_by_path,
614        available_style_paths,
615        package_manifests,
616    );
617    let (closure_paths, cycle_paths) =
618        collect_hypergraph_transitive_closure_paths(&graph, css_modules_value_node_label);
619    let mut closure_edges = closure_paths
620        .into_iter()
621        .map(
622            |HypergraphClosurePath {
623                 origin,
624                 target,
625                 depth,
626                 path_labels,
627             }| CssModulesValueClosureEdgeV0 {
628                from_style_path: origin.style_path,
629                value_name: origin.value_name,
630                target_style_path: target.style_path,
631                target_value_name: target.value_name,
632                depth,
633                path: path_labels,
634            },
635        )
636        .collect::<Vec<_>>();
637    let mut cycles = cycle_paths
638        .into_iter()
639        .map(|path| CssModulesCycleV0 {
640            kind: "value",
641            path,
642        })
643        .collect::<Vec<_>>();
644
645    closure_edges.sort_by_key(|edge| {
646        (
647            edge.from_style_path.clone(),
648            edge.value_name.clone(),
649            edge.depth,
650            edge.target_style_path.clone(),
651            edge.target_value_name.clone(),
652        )
653    });
654    cycles.sort_by_key(|cycle| cycle.path.clone());
655    (closure_edges, cycles)
656}
657
658fn collect_css_modules_value_adjacency(
659    facts_by_path: &BTreeMap<&str, &CssModulesCrossFileStyleFactsV0>,
660    available_style_paths: &BTreeSet<&str>,
661    package_manifests: &[OmenaResolverStylePackageManifestV0],
662) -> BTreeMap<CssModulesValueNode, BTreeSet<CssModulesValueNode>> {
663    let mut graph = BTreeMap::new();
664    for (style_path, facts) in facts_by_path {
665        let local_value_names = facts
666            .css_module_value_definition_names
667            .iter()
668            .chain(
669                facts
670                    .css_module_value_import_edges
671                    .iter()
672                    .map(|edge| &edge.local_name),
673            )
674            .map(String::as_str)
675            .collect::<BTreeSet<_>>();
676        for edge in &facts.css_module_value_definition_edges {
677            if !local_value_names.contains(edge.definition_name.as_str()) {
678                continue;
679            }
680            let owner = CssModulesValueNode {
681                style_path: (*style_path).to_string(),
682                value_name: edge.definition_name.clone(),
683            };
684            for reference_name in &edge.reference_names {
685                if !local_value_names.contains(reference_name.as_str()) {
686                    continue;
687                }
688                graph
689                    .entry(owner.clone())
690                    .or_insert_with(BTreeSet::new)
691                    .insert(CssModulesValueNode {
692                        style_path: (*style_path).to_string(),
693                        value_name: reference_name.clone(),
694                    });
695            }
696        }
697
698        for edge in &facts.css_module_value_import_edges {
699            let Some(target_style_path) = resolve_omena_resolver_style_module_source(
700                style_path,
701                edge.import_source.as_str(),
702                available_style_paths,
703                package_manifests,
704            ) else {
705                continue;
706            };
707            let Some(target_facts) = facts_by_path.get(target_style_path.as_str()) else {
708                continue;
709            };
710            if !target_facts
711                .css_module_value_definition_names
712                .iter()
713                .any(|name| name == &edge.remote_name)
714            {
715                continue;
716            }
717            graph
718                .entry(CssModulesValueNode {
719                    style_path: (*style_path).to_string(),
720                    value_name: edge.local_name.clone(),
721                })
722                .or_insert_with(BTreeSet::new)
723                .insert(CssModulesValueNode {
724                    style_path: target_style_path,
725                    value_name: edge.remote_name.clone(),
726                });
727        }
728    }
729    graph
730}
731
732fn css_modules_value_node_label(node: &CssModulesValueNode) -> String {
733    format!("{}#{}", node.style_path, node.value_name)
734}
735
736#[derive(Debug, Clone, PartialEq, Eq, PartialOrd, Ord)]
737struct CssModulesIcssNode {
738    style_path: String,
739    name: String,
740}
741
742fn summarize_css_modules_icss_closure(
743    facts_by_path: &BTreeMap<&str, &CssModulesCrossFileStyleFactsV0>,
744    available_style_paths: &BTreeSet<&str>,
745    package_manifests: &[OmenaResolverStylePackageManifestV0],
746) -> (Vec<CssModulesIcssClosureEdgeV0>, Vec<CssModulesCycleV0>) {
747    let graph =
748        collect_css_modules_icss_adjacency(facts_by_path, available_style_paths, package_manifests);
749    let (closure_paths, cycle_paths) =
750        collect_hypergraph_transitive_closure_paths(&graph, css_modules_icss_node_label);
751    let mut closure_edges = closure_paths
752        .into_iter()
753        .map(
754            |HypergraphClosurePath {
755                 origin,
756                 target,
757                 depth,
758                 path_labels,
759             }| CssModulesIcssClosureEdgeV0 {
760                from_style_path: origin.style_path,
761                name: origin.name,
762                target_style_path: target.style_path,
763                target_name: target.name,
764                depth,
765                path: path_labels,
766            },
767        )
768        .collect::<Vec<_>>();
769    let mut cycles = cycle_paths
770        .into_iter()
771        .map(|path| CssModulesCycleV0 { kind: "icss", path })
772        .collect::<Vec<_>>();
773
774    closure_edges.sort_by_key(|edge| {
775        (
776            edge.from_style_path.clone(),
777            edge.name.clone(),
778            edge.depth,
779            edge.target_style_path.clone(),
780            edge.target_name.clone(),
781        )
782    });
783    cycles.sort_by_key(|cycle| cycle.path.clone());
784    (closure_edges, cycles)
785}
786
787fn collect_css_modules_icss_adjacency(
788    facts_by_path: &BTreeMap<&str, &CssModulesCrossFileStyleFactsV0>,
789    available_style_paths: &BTreeSet<&str>,
790    package_manifests: &[OmenaResolverStylePackageManifestV0],
791) -> BTreeMap<CssModulesIcssNode, BTreeSet<CssModulesIcssNode>> {
792    let mut graph = BTreeMap::new();
793    for (style_path, facts) in facts_by_path {
794        let local_names = facts
795            .icss_export_names
796            .iter()
797            .chain(facts.icss_import_edges.iter().map(|edge| &edge.local_name))
798            .map(String::as_str)
799            .collect::<BTreeSet<_>>();
800        for edge in &facts.icss_export_edges {
801            if !local_names.contains(edge.export_name.as_str()) {
802                continue;
803            }
804            let owner = CssModulesIcssNode {
805                style_path: (*style_path).to_string(),
806                name: edge.export_name.clone(),
807            };
808            for reference_name in &edge.reference_names {
809                if !local_names.contains(reference_name.as_str()) {
810                    continue;
811                }
812                graph
813                    .entry(owner.clone())
814                    .or_insert_with(BTreeSet::new)
815                    .insert(CssModulesIcssNode {
816                        style_path: (*style_path).to_string(),
817                        name: reference_name.clone(),
818                    });
819            }
820        }
821
822        for edge in &facts.icss_import_edges {
823            let Some(target_style_path) = resolve_omena_resolver_style_module_source(
824                style_path,
825                edge.import_source.as_str(),
826                available_style_paths,
827                package_manifests,
828            ) else {
829                continue;
830            };
831            let Some(target_facts) = facts_by_path.get(target_style_path.as_str()) else {
832                continue;
833            };
834            if !target_facts
835                .icss_export_names
836                .iter()
837                .any(|name| name == &edge.remote_name)
838            {
839                continue;
840            }
841            graph
842                .entry(CssModulesIcssNode {
843                    style_path: (*style_path).to_string(),
844                    name: edge.local_name.clone(),
845                })
846                .or_insert_with(BTreeSet::new)
847                .insert(CssModulesIcssNode {
848                    style_path: target_style_path,
849                    name: edge.remote_name.clone(),
850                });
851        }
852    }
853    graph
854}
855
856fn css_modules_icss_node_label(node: &CssModulesIcssNode) -> String {
857    format!("{}#{}", node.style_path, node.name)
858}