Skip to main content

omena_query/style/
cross_file_summary.rs

1use super::*;
2
3pub(super) fn summarize_omena_query_cross_file_summary(
4    style_fact_entries: &[OmenaQueryStyleFactEntry],
5    css_modules_resolution: &OmenaQueryCssModulesCrossFileResolutionV0,
6    sass_module_resolution: &OmenaQuerySassModuleCrossFileResolutionV0,
7) -> OmenaQueryCrossFileSummaryV0 {
8    let mut edges = Vec::new();
9
10    for edge in &css_modules_resolution.edges {
11        let edge_kind = match edge.import_kind {
12            "composes" => "cssModulesComposesImport",
13            "value" => "cssModulesValueImport",
14            "icss" => "cssModulesIcssImport",
15            _ => "cssModulesImport",
16        };
17        let provenance = match edge.import_kind {
18            "composes" => vec![
19                "omena-query.css-modules-cross-file-resolution",
20                "omena-parser.css-module-composes-facts",
21            ],
22            "value" => vec![
23                "omena-query.css-modules-cross-file-resolution",
24                "omena-parser.css-module-value-facts",
25            ],
26            "icss" => vec![
27                "omena-query.css-modules-cross-file-resolution",
28                "omena-parser.icss-facts",
29            ],
30            _ => vec!["omena-query.css-modules-cross-file-resolution"],
31        };
32        edges.push(build_omena_query_cross_file_summary_edge(
33            OmenaQueryCrossFileSummaryEdgeInput {
34                edge_kind,
35                from_kind: "style",
36                from_path: edge.from_style_path.clone(),
37                target_kind: edge.resolved_style_path.as_ref().map(|_| "style"),
38                target_path: edge.resolved_style_path.clone(),
39                source: Some(edge.source.clone()),
40                owner_selector_name: None,
41                local_name: None,
42                remote_name: None,
43                target_names: edge.imported_names.clone(),
44                status: edge.status,
45                provenance,
46            },
47        ));
48    }
49
50    for edge in &css_modules_resolution.composes_closure_edges {
51        edges.push(build_omena_query_cross_file_summary_edge(
52            OmenaQueryCrossFileSummaryEdgeInput {
53                edge_kind: "cssModulesComposesClosure",
54                from_kind: "style",
55                from_path: edge.from_style_path.clone(),
56                target_kind: Some("style"),
57                target_path: Some(edge.target_style_path.clone()),
58                source: None,
59                owner_selector_name: Some(edge.owner_selector_name.clone()),
60                local_name: None,
61                remote_name: Some(edge.target_selector_name.clone()),
62                target_names: vec![edge.target_selector_name.clone()],
63                status: "reachable",
64                provenance: vec![
65                    "omena-query.css-modules-cross-file-resolution",
66                    "omena-parser.css-module-composes-facts",
67                ],
68            },
69        ));
70    }
71
72    for edge in &css_modules_resolution.value_closure_edges {
73        edges.push(build_omena_query_cross_file_summary_edge(
74            OmenaQueryCrossFileSummaryEdgeInput {
75                edge_kind: "cssModulesValueClosure",
76                from_kind: "style",
77                from_path: edge.from_style_path.clone(),
78                target_kind: Some("style"),
79                target_path: Some(edge.target_style_path.clone()),
80                source: None,
81                owner_selector_name: None,
82                local_name: Some(edge.value_name.clone()),
83                remote_name: Some(edge.target_value_name.clone()),
84                target_names: vec![edge.target_value_name.clone()],
85                status: "reachable",
86                provenance: vec![
87                    "omena-query.css-modules-cross-file-resolution",
88                    "omena-parser.css-module-value-facts",
89                ],
90            },
91        ));
92    }
93
94    for edge in &css_modules_resolution.icss_closure_edges {
95        edges.push(build_omena_query_cross_file_summary_edge(
96            OmenaQueryCrossFileSummaryEdgeInput {
97                edge_kind: "cssModulesIcssClosure",
98                from_kind: "style",
99                from_path: edge.from_style_path.clone(),
100                target_kind: Some("style"),
101                target_path: Some(edge.target_style_path.clone()),
102                source: None,
103                owner_selector_name: None,
104                local_name: Some(edge.name.clone()),
105                remote_name: Some(edge.target_name.clone()),
106                target_names: vec![edge.target_name.clone()],
107                status: "reachable",
108                provenance: vec![
109                    "omena-query.css-modules-cross-file-resolution",
110                    "omena-parser.icss-facts",
111                ],
112            },
113        ));
114    }
115
116    for edge in &sass_module_resolution.edges {
117        edges.push(build_omena_query_cross_file_summary_edge(
118            OmenaQueryCrossFileSummaryEdgeInput {
119                edge_kind: edge.edge_kind,
120                from_kind: "style",
121                from_path: edge.from_style_path.clone(),
122                target_kind: edge.resolved_style_path.as_ref().map(|_| "style"),
123                target_path: edge.resolved_style_path.clone(),
124                source: Some(edge.source.clone()),
125                owner_selector_name: None,
126                local_name: edge.namespace.clone(),
127                remote_name: edge.forward_prefix.clone(),
128                target_names: edge.visibility_filter_names.clone(),
129                status: edge.status,
130                provenance: vec![
131                    "omena-query.sass-module-cross-file-resolution",
132                    "omena-parser.sass-module-facts",
133                ],
134            },
135        ));
136    }
137
138    for edge in &sass_module_resolution.graph_closure_edges {
139        edges.push(build_omena_query_cross_file_summary_edge(
140            OmenaQueryCrossFileSummaryEdgeInput {
141                edge_kind: "sassModuleGraphClosure",
142                from_kind: "style",
143                from_path: edge.from_style_path.clone(),
144                target_kind: Some("style"),
145                target_path: Some(edge.target_style_path.clone()),
146                source: None,
147                owner_selector_name: None,
148                local_name: edge.namespace.clone(),
149                remote_name: edge.forward_prefix.clone(),
150                target_names: edge.visibility_filter_names.clone(),
151                status: "reachable",
152                provenance: vec![
153                    "omena-query.sass-module-cross-file-resolution",
154                    "omena-parser.sass-module-facts",
155                ],
156            },
157        ));
158    }
159
160    let design_token_declarations = collect_design_token_declarations_by_name(style_fact_entries);
161    let design_token_reachability =
162        collect_design_token_reachable_style_paths_by_origin(sass_module_resolution);
163
164    for entry in style_fact_entries {
165        let local_declarations = entry
166            .facts
167            .custom_property_decl_names
168            .iter()
169            .map(String::as_str)
170            .collect::<BTreeSet<_>>();
171        for name in &entry.facts.custom_property_ref_names {
172            let target = resolve_design_token_reference_target(
173                entry.style_path.as_str(),
174                name.as_str(),
175                &local_declarations,
176                &design_token_declarations,
177                &design_token_reachability,
178            );
179            let provenance = target.provenance();
180            let target_style_path = target.target_style_path;
181            let status = target.status;
182            edges.push(build_omena_query_cross_file_summary_edge(
183                OmenaQueryCrossFileSummaryEdgeInput {
184                    edge_kind: "styleDesignTokenReference",
185                    from_kind: "style",
186                    from_path: entry.style_path.clone(),
187                    target_kind: target_style_path.as_ref().map(|_| "style"),
188                    target_path: target_style_path,
189                    source: None,
190                    owner_selector_name: None,
191                    local_name: Some(name.clone()),
192                    remote_name: None,
193                    target_names: vec![name.clone()],
194                    status,
195                    provenance,
196                },
197            ));
198        }
199    }
200
201    edges.sort_by_key(|edge| edge.edge_id.clone());
202    let summary_hash = stable_omena_query_cross_file_summary_hash(&edges);
203
204    OmenaQueryCrossFileSummaryV0 {
205        schema_version: "0",
206        product: "omena-query.cross-file-summary",
207        status: "summaryEdgeSeed",
208        summary_scope: "styleSemanticGraphBatch",
209        style_count: style_fact_entries.len(),
210        summary_edge_count: edges.len(),
211        edge_kind_counts: summarize_omena_query_cross_file_summary_edge_kind_counts(&edges),
212        summary_hash,
213        edges,
214        capabilities: OmenaQueryCrossFileSummaryCapabilitiesV0 {
215            css_modules_composes_edges_ready: true,
216            css_modules_value_edges_ready: true,
217            css_modules_icss_edges_ready: true,
218            sass_module_edges_ready: true,
219            style_design_token_reference_edges_ready: true,
220            source_selector_reference_edges_ready: false,
221            stable_summary_hash_ready: true,
222            linear_provenance_ready: true,
223            linear_provenance_round_trip_ready: true,
224            linear_provenance_semiring_laws_hold:
225                summarize_omena_query_linear_provenance_semiring_laws().all_fixture_laws_hold,
226        },
227        next_priorities: vec![
228            "sourceSelectorReferenceSummaryEdges",
229            "summaryEdgeEquivalenceGate",
230        ],
231    }
232}
233
234#[derive(Debug, Clone)]
235struct DesignTokenReferenceTarget {
236    target_style_path: Option<String>,
237    status: &'static str,
238    resolution_provenance: Option<&'static str>,
239}
240
241impl DesignTokenReferenceTarget {
242    fn provenance(&self) -> Vec<&'static str> {
243        let mut provenance = vec![
244            "omena-query.style-semantic-graph-batch",
245            "omena-parser.custom-property-facts",
246        ];
247        if let Some(resolution_provenance) = self.resolution_provenance {
248            provenance.push(resolution_provenance);
249        }
250        provenance
251    }
252}
253
254#[derive(Debug, Clone, PartialEq, Eq, PartialOrd, Ord)]
255struct DesignTokenReachableStylePath {
256    distance: usize,
257    target_style_path: String,
258}
259
260fn collect_design_token_declarations_by_name(
261    style_fact_entries: &[OmenaQueryStyleFactEntry],
262) -> BTreeMap<String, BTreeSet<String>> {
263    let mut declarations_by_name = BTreeMap::<String, BTreeSet<String>>::new();
264    for entry in style_fact_entries {
265        for name in &entry.facts.custom_property_decl_names {
266            declarations_by_name
267                .entry(name.clone())
268                .or_default()
269                .insert(entry.style_path.clone());
270        }
271    }
272    declarations_by_name
273}
274
275fn collect_design_token_reachable_style_paths_by_origin(
276    sass_module_resolution: &OmenaQuerySassModuleCrossFileResolutionV0,
277) -> BTreeMap<String, Vec<DesignTokenReachableStylePath>> {
278    let mut reachable_by_origin =
279        BTreeMap::<String, BTreeSet<DesignTokenReachableStylePath>>::new();
280
281    for edge in &sass_module_resolution.edges {
282        if edge.status != "resolved" {
283            continue;
284        }
285        let Some(target_style_path) = edge.resolved_style_path.as_ref() else {
286            continue;
287        };
288        reachable_by_origin
289            .entry(edge.from_style_path.clone())
290            .or_default()
291            .insert(DesignTokenReachableStylePath {
292                distance: 1,
293                target_style_path: target_style_path.clone(),
294            });
295    }
296
297    for edge in &sass_module_resolution.graph_closure_edges {
298        reachable_by_origin
299            .entry(edge.from_style_path.clone())
300            .or_default()
301            .insert(DesignTokenReachableStylePath {
302                distance: edge.depth,
303                target_style_path: edge.target_style_path.clone(),
304            });
305    }
306
307    reachable_by_origin
308        .into_iter()
309        .map(|(origin, reachable)| (origin, reachable.into_iter().collect()))
310        .collect()
311}
312
313fn resolve_design_token_reference_target(
314    from_style_path: &str,
315    name: &str,
316    local_declarations: &BTreeSet<&str>,
317    declarations_by_name: &BTreeMap<String, BTreeSet<String>>,
318    reachable_by_origin: &BTreeMap<String, Vec<DesignTokenReachableStylePath>>,
319) -> DesignTokenReferenceTarget {
320    if local_declarations.contains(name) {
321        return DesignTokenReferenceTarget {
322            target_style_path: Some(from_style_path.to_string()),
323            status: "localResolved",
324            resolution_provenance: None,
325        };
326    }
327
328    let Some(declaration_paths) = declarations_by_name.get(name) else {
329        return DesignTokenReferenceTarget {
330            target_style_path: None,
331            status: "unresolvedReference",
332            resolution_provenance: None,
333        };
334    };
335
336    let Some(reachable_paths) = reachable_by_origin.get(from_style_path) else {
337        return DesignTokenReferenceTarget {
338            target_style_path: None,
339            status: "unresolvedReference",
340            resolution_provenance: None,
341        };
342    };
343
344    let target_style_path = reachable_paths
345        .iter()
346        .filter(|reachable| declaration_paths.contains(reachable.target_style_path.as_str()))
347        .min_by_key(|reachable| (reachable.distance, reachable.target_style_path.as_str()))
348        .map(|reachable| reachable.target_style_path.clone());
349
350    if let Some(target_style_path) = target_style_path {
351        DesignTokenReferenceTarget {
352            target_style_path: Some(target_style_path),
353            status: "importResolved",
354            resolution_provenance: Some("omena-query.sass-module-cross-file-resolution"),
355        }
356    } else {
357        DesignTokenReferenceTarget {
358            target_style_path: None,
359            status: "unresolvedReference",
360            resolution_provenance: None,
361        }
362    }
363}
364
365pub fn summarize_omena_query_source_selector_reference_cross_file_summary(
366    style_sources: &[OmenaQueryStyleSourceInputV0],
367    source_documents: &[OmenaQuerySourceDocumentInputV0],
368    package_manifests: &[OmenaQueryStylePackageManifestV0],
369) -> OmenaQueryCrossFileSummaryV0 {
370    let definitions =
371        super::source_refs::summarize_omena_query_style_selector_definitions(style_sources);
372    let references = super::source_refs::collect_omena_query_source_selector_references(
373        style_sources,
374        source_documents,
375        package_manifests,
376    );
377    let mut edges = references
378        .into_iter()
379        .map(|reference| {
380            let candidate = reference.candidate;
381            let source_candidate = OmenaQuerySourceSelectorCandidateV0 {
382                kind: candidate.kind,
383                name: candidate.name.clone(),
384                range: candidate.range,
385                source: candidate.source,
386                target_style_uri: candidate.target_style_uri.clone(),
387            };
388            let matched_definitions =
389                resolve_omena_query_style_selector_definitions_for_source_candidate(
390                    &source_candidate,
391                    definitions.as_slice(),
392                );
393            let target_names = if candidate.kind == "sourceSelectorPrefixReference" {
394                matched_definitions
395                    .iter()
396                    .map(|definition| definition.name.clone())
397                    .collect::<Vec<_>>()
398            } else {
399                vec![candidate.name.clone()]
400            };
401            let status = if matched_definitions.is_empty() {
402                "unresolved"
403            } else {
404                "resolved"
405            };
406            build_omena_query_cross_file_summary_edge(OmenaQueryCrossFileSummaryEdgeInput {
407                edge_kind: candidate.kind,
408                from_kind: "source",
409                from_path: candidate.uri,
410                target_kind: candidate.target_style_uri.as_ref().map(|_| "style"),
411                target_path: candidate.target_style_uri,
412                source: None,
413                owner_selector_name: None,
414                local_name: Some(candidate.name),
415                remote_name: None,
416                target_names,
417                status,
418                provenance: vec![
419                    "omena-query.source-selector-references",
420                    "omena-query.style-selector-definitions",
421                ],
422            })
423        })
424        .collect::<Vec<_>>();
425
426    edges.sort_by_key(|edge| edge.edge_id.clone());
427    let summary_hash = stable_omena_query_cross_file_summary_hash(&edges);
428
429    OmenaQueryCrossFileSummaryV0 {
430        schema_version: "0",
431        product: "omena-query.cross-file-summary",
432        status: "sourceSelectorSummaryEdgeSeed",
433        summary_scope: "sourceSelectorReferences",
434        style_count: style_sources.len(),
435        summary_edge_count: edges.len(),
436        edge_kind_counts: summarize_omena_query_cross_file_summary_edge_kind_counts(&edges),
437        summary_hash,
438        edges,
439        capabilities: OmenaQueryCrossFileSummaryCapabilitiesV0 {
440            css_modules_composes_edges_ready: false,
441            css_modules_value_edges_ready: false,
442            css_modules_icss_edges_ready: false,
443            sass_module_edges_ready: false,
444            style_design_token_reference_edges_ready: false,
445            source_selector_reference_edges_ready: true,
446            stable_summary_hash_ready: true,
447            linear_provenance_ready: true,
448            linear_provenance_round_trip_ready: true,
449            linear_provenance_semiring_laws_hold:
450                summarize_omena_query_linear_provenance_semiring_laws().all_fixture_laws_hold,
451        },
452        next_priorities: vec!["sourceSelectorReferenceSummaryEquivalenceGate"],
453    }
454}
455
456pub fn summarize_omena_query_workspace_cross_file_summary(
457    style_sources: &[OmenaQueryStyleSourceInputV0],
458    source_documents: &[OmenaQuerySourceDocumentInputV0],
459    package_manifests: &[OmenaQueryStylePackageManifestV0],
460) -> OmenaQueryCrossFileSummaryV0 {
461    let style_pairs = style_sources
462        .iter()
463        .map(|source| (source.style_path.as_str(), source.style_source.as_str()))
464        .collect::<Vec<_>>();
465    let style_fact_entries = super::collect_omena_query_style_fact_entries(style_pairs.as_slice());
466    let css_modules_resolution =
467        super::summarize_css_modules_cross_file_resolution(&style_fact_entries, package_manifests);
468    let sass_module_resolution =
469        super::summarize_sass_module_cross_file_resolution(&style_fact_entries, package_manifests);
470    let style_summary = summarize_omena_query_cross_file_summary(
471        &style_fact_entries,
472        &css_modules_resolution,
473        &sass_module_resolution,
474    );
475    let source_summary = summarize_omena_query_source_selector_reference_cross_file_summary(
476        style_sources,
477        source_documents,
478        package_manifests,
479    );
480
481    merge_omena_query_cross_file_summaries(
482        "workspaceSummaryEdgeSeed",
483        "workspaceStyleAndSource",
484        style_sources.len(),
485        &[style_summary, source_summary],
486    )
487}
488
489const M4_AXIS_C_REQUIRED_EDGE_KINDS: [&str; 12] = [
490    "cssModulesComposesImport",
491    "cssModulesComposesClosure",
492    "cssModulesValueImport",
493    "cssModulesValueClosure",
494    "cssModulesIcssImport",
495    "cssModulesIcssClosure",
496    "sassUse",
497    "sassForward",
498    "sassImport",
499    "sassModuleGraphClosure",
500    "styleDesignTokenReference",
501    "sourceSelectorReference",
502];
503
504pub fn summarize_omena_query_m4_axis_c_readiness() -> OmenaQueryM4AxisCReadinessSummaryV0 {
505    let button_style_source = m4_axis_c_button_style_source("./base.module.scss");
506    let style_sources = m4_axis_c_style_sources(button_style_source.as_str());
507    let source_documents = m4_axis_c_source_documents("styles.root");
508    let package_manifests = m4_axis_c_package_manifests("./dist/theme.css");
509    let style_pairs = style_sources
510        .iter()
511        .map(|source| (source.style_path.as_str(), source.style_source.as_str()))
512        .collect::<Vec<_>>();
513    let style_fact_entries = super::collect_omena_query_style_fact_entries(style_pairs.as_slice());
514    let css_modules_resolution =
515        super::summarize_css_modules_cross_file_resolution(&style_fact_entries, &package_manifests);
516    let sass_module_resolution =
517        super::summarize_sass_module_cross_file_resolution(&style_fact_entries, &package_manifests);
518    let style_summary = summarize_omena_query_cross_file_summary(
519        &style_fact_entries,
520        &css_modules_resolution,
521        &sass_module_resolution,
522    );
523    let source_summary = summarize_omena_query_source_selector_reference_cross_file_summary(
524        style_sources.as_slice(),
525        source_documents.as_slice(),
526        package_manifests.as_slice(),
527    );
528    let workspace_summary = merge_omena_query_cross_file_summaries(
529        "workspaceSummaryEdgeSeed",
530        "workspaceStyleAndSource",
531        style_sources.len(),
532        &[style_summary.clone(), source_summary.clone()],
533    );
534
535    let required_edge_kind_counts =
536        m4_axis_c_required_edge_kind_counts(&workspace_summary.edge_kind_counts);
537    let required_edge_kinds_ready = required_edge_kind_counts
538        .iter()
539        .all(|entry| entry.count > 0);
540    let issue_63_provenance_round_trip_ready =
541        workspace_summary.capabilities.linear_provenance_ready
542            && workspace_summary
543                .capabilities
544                .linear_provenance_round_trip_ready
545            && workspace_summary.linear_provenance_round_trips_legacy_labels();
546    let issue_65_summary_edge_equivalence_ready = required_edge_kinds_ready
547        && style_summary.summary_edge_count
548            == m4_axis_c_expected_style_summary_edge_count(
549                &style_fact_entries,
550                &css_modules_resolution,
551                &sass_module_resolution,
552            )
553        && source_summary.edges.iter().any(|edge| {
554            edge.edge_kind == "sourceSelectorReference"
555                && edge.local_name.as_deref() == Some("root")
556                && edge.status == "resolved"
557        })
558        && workspace_summary.summary_edge_count
559            == m4_axis_c_merged_edge_id_count(&style_summary, &source_summary);
560
561    let source_changed = summarize_omena_query_workspace_cross_file_summary(
562        style_sources.as_slice(),
563        m4_axis_c_source_documents("styles.missing").as_slice(),
564        package_manifests.as_slice(),
565    );
566    let style_changed = summarize_omena_query_workspace_cross_file_summary(
567        m4_axis_c_style_sources(m4_axis_c_button_style_source("./missing.module.scss").as_str())
568            .as_slice(),
569        source_documents.as_slice(),
570        package_manifests.as_slice(),
571    );
572    let package_changed = summarize_omena_query_workspace_cross_file_summary(
573        style_sources.as_slice(),
574        source_documents.as_slice(),
575        m4_axis_c_package_manifests("./dist/alt.css").as_slice(),
576    );
577    let summary_hash_invalidation_ready = workspace_summary.summary_hash
578        != source_changed.summary_hash
579        && workspace_summary.summary_hash != style_changed.summary_hash
580        && workspace_summary.summary_hash != package_changed.summary_hash;
581    let ready = issue_63_provenance_round_trip_ready
582        && issue_65_summary_edge_equivalence_ready
583        && summary_hash_invalidation_ready;
584
585    OmenaQueryM4AxisCReadinessSummaryV0 {
586        schema_version: "0",
587        product: "omena-query.m4-axis-c-readiness",
588        status: if ready {
589            "m4AxisCReady"
590        } else {
591            "m4AxisCNeedsWork"
592        },
593        required_edge_kind_count: M4_AXIS_C_REQUIRED_EDGE_KINDS.len(),
594        required_edge_kind_counts,
595        workspace_edge_count: workspace_summary.summary_edge_count,
596        issue_63_provenance_round_trip_ready,
597        issue_65_summary_edge_equivalence_ready,
598        summary_hash_invalidation_ready,
599        summary_hash_samples: OmenaQueryM4AxisCSummaryHashSamplesV0 {
600            baseline: workspace_summary.summary_hash,
601            source_selector_change: source_changed.summary_hash,
602            style_edge_change: style_changed.summary_hash,
603            package_manifest_change: package_changed.summary_hash,
604        },
605        checked_surfaces: vec![
606            "linear-provenance-round-trip",
607            "summary-edge-resolution-equivalence",
608            "source-selector-summary-edge",
609            "workspace-summary-hash-source-invalidation",
610            "workspace-summary-hash-style-invalidation",
611            "workspace-summary-hash-package-manifest-invalidation",
612        ],
613        next_priorities: if ready {
614            vec![]
615        } else {
616            vec![
617                "completeM4AxisCProvenanceRoundTrip",
618                "completeM4AxisCSummaryEdgeEquivalence",
619                "completeM4AxisCSummaryHashInvalidation",
620            ]
621        },
622    }
623}
624
625fn m4_axis_c_button_style_source(composes_target: &str) -> String {
626    format!(
627        r#"@use "@design/tokens/theme";
628@forward "./palette";
629@import "./legacy";
630@value primary as localPrimary from "./tokens.module.scss";
631:import("./tokens.module.scss") {{ imported: exported; }}
632:export {{ forwarded: imported; }}
633.root {{ composes: base from "{composes_target}"; color: var(--brand); }}"#
634    )
635}
636
637fn m4_axis_c_style_sources(button_style_source: &str) -> Vec<OmenaQueryStyleSourceInputV0> {
638    vec![
639        OmenaQueryStyleSourceInputV0 {
640            style_path: "/fake/workspace/node_modules/@design/tokens/dist/theme.css".to_string(),
641            style_source: ":root { --brand: theme; }".to_string(),
642        },
643        OmenaQueryStyleSourceInputV0 {
644            style_path: "/fake/workspace/node_modules/@design/tokens/dist/alt.css".to_string(),
645            style_source: ":root { --brand: alt; }".to_string(),
646        },
647        OmenaQueryStyleSourceInputV0 {
648            style_path: "/fake/workspace/src/base.module.scss".to_string(),
649            style_source: ".base { display: block; }".to_string(),
650        },
651        OmenaQueryStyleSourceInputV0 {
652            style_path: "/fake/workspace/src/tokens.module.scss".to_string(),
653            style_source: "@value primary: red; :export { raw: red; exported: raw; }".to_string(),
654        },
655        OmenaQueryStyleSourceInputV0 {
656            style_path: "/fake/workspace/src/_palette.scss".to_string(),
657            style_source: "$tone: red;".to_string(),
658        },
659        OmenaQueryStyleSourceInputV0 {
660            style_path: "/fake/workspace/src/_legacy.scss".to_string(),
661            style_source: "$legacy: blue;".to_string(),
662        },
663        OmenaQueryStyleSourceInputV0 {
664            style_path: "/fake/workspace/src/Button.module.scss".to_string(),
665            style_source: button_style_source.to_string(),
666        },
667    ]
668}
669
670fn m4_axis_c_source_documents(class_expression: &str) -> Vec<OmenaQuerySourceDocumentInputV0> {
671    vec![OmenaQuerySourceDocumentInputV0 {
672        source_path: "/fake/workspace/src/Button.tsx".to_string(),
673        source_source: format!(
674            "import styles from './Button.module.scss';\nconst cls = {class_expression};\n"
675        ),
676    }]
677}
678
679fn m4_axis_c_package_manifests(style_export_target: &str) -> Vec<OmenaQueryStylePackageManifestV0> {
680    vec![OmenaQueryStylePackageManifestV0 {
681        package_json_path: "/fake/workspace/node_modules/@design/tokens/package.json".to_string(),
682        package_json_source: format!(
683            r#"{{"exports":{{"./theme":{{"style":"{style_export_target}"}}}}}}"#
684        ),
685    }]
686}
687
688fn m4_axis_c_required_edge_kind_counts(
689    edge_kind_counts: &[OmenaQueryCrossFileSummaryEdgeKindCountV0],
690) -> Vec<OmenaQueryCrossFileSummaryEdgeKindCountV0> {
691    let observed = edge_kind_counts
692        .iter()
693        .map(|entry| (entry.edge_kind, entry.count))
694        .collect::<BTreeMap<_, _>>();
695    M4_AXIS_C_REQUIRED_EDGE_KINDS
696        .iter()
697        .map(|edge_kind| OmenaQueryCrossFileSummaryEdgeKindCountV0 {
698            edge_kind,
699            count: observed.get(edge_kind).copied().unwrap_or(0),
700        })
701        .collect()
702}
703
704fn m4_axis_c_expected_style_summary_edge_count(
705    style_fact_entries: &[super::OmenaQueryStyleFactEntry],
706    css_modules_resolution: &OmenaQueryCssModulesCrossFileResolutionV0,
707    sass_module_resolution: &OmenaQuerySassModuleCrossFileResolutionV0,
708) -> usize {
709    let custom_property_reference_count = style_fact_entries
710        .iter()
711        .map(|entry| entry.facts.custom_property_ref_names.len())
712        .sum::<usize>();
713
714    css_modules_resolution.edges.len()
715        + css_modules_resolution.composes_closure_edges.len()
716        + css_modules_resolution.value_closure_edges.len()
717        + css_modules_resolution.icss_closure_edges.len()
718        + sass_module_resolution.edges.len()
719        + sass_module_resolution.graph_closure_edges.len()
720        + custom_property_reference_count
721}
722
723fn m4_axis_c_merged_edge_id_count(
724    style_summary: &OmenaQueryCrossFileSummaryV0,
725    source_summary: &OmenaQueryCrossFileSummaryV0,
726) -> usize {
727    style_summary
728        .edges
729        .iter()
730        .chain(source_summary.edges.iter())
731        .map(|edge| edge.edge_id.as_str())
732        .collect::<BTreeSet<_>>()
733        .len()
734}
735
736fn merge_omena_query_cross_file_summaries(
737    status: &'static str,
738    summary_scope: &'static str,
739    style_count: usize,
740    summaries: &[OmenaQueryCrossFileSummaryV0],
741) -> OmenaQueryCrossFileSummaryV0 {
742    let mut edges = summaries
743        .iter()
744        .flat_map(|summary| summary.edges.clone())
745        .collect::<Vec<_>>();
746    edges.sort_by_key(|edge| edge.edge_id.clone());
747    edges.dedup_by(|left, right| left.edge_id == right.edge_id);
748    let summary_hash = stable_omena_query_cross_file_summary_hash(edges.as_slice());
749
750    OmenaQueryCrossFileSummaryV0 {
751        schema_version: "0",
752        product: "omena-query.cross-file-summary",
753        status,
754        summary_scope,
755        style_count,
756        summary_edge_count: edges.len(),
757        edge_kind_counts: summarize_omena_query_cross_file_summary_edge_kind_counts(&edges),
758        summary_hash,
759        edges,
760        capabilities: merge_omena_query_cross_file_summary_capabilities(summaries),
761        next_priorities: vec!["workspaceSummaryHashInvalidationGate"],
762    }
763}
764
765fn merge_omena_query_cross_file_summary_capabilities(
766    summaries: &[OmenaQueryCrossFileSummaryV0],
767) -> OmenaQueryCrossFileSummaryCapabilitiesV0 {
768    OmenaQueryCrossFileSummaryCapabilitiesV0 {
769        css_modules_composes_edges_ready: summaries
770            .iter()
771            .any(|summary| summary.capabilities.css_modules_composes_edges_ready),
772        css_modules_value_edges_ready: summaries
773            .iter()
774            .any(|summary| summary.capabilities.css_modules_value_edges_ready),
775        css_modules_icss_edges_ready: summaries
776            .iter()
777            .any(|summary| summary.capabilities.css_modules_icss_edges_ready),
778        sass_module_edges_ready: summaries
779            .iter()
780            .any(|summary| summary.capabilities.sass_module_edges_ready),
781        style_design_token_reference_edges_ready: summaries.iter().any(|summary| {
782            summary
783                .capabilities
784                .style_design_token_reference_edges_ready
785        }),
786        source_selector_reference_edges_ready: summaries
787            .iter()
788            .any(|summary| summary.capabilities.source_selector_reference_edges_ready),
789        stable_summary_hash_ready: summaries
790            .iter()
791            .all(|summary| summary.capabilities.stable_summary_hash_ready),
792        linear_provenance_ready: summaries
793            .iter()
794            .all(|summary| summary.capabilities.linear_provenance_ready),
795        linear_provenance_round_trip_ready: summaries
796            .iter()
797            .all(|summary| summary.capabilities.linear_provenance_round_trip_ready)
798            && summaries
799                .iter()
800                .all(OmenaQueryCrossFileSummaryV0::linear_provenance_round_trips_legacy_labels),
801        linear_provenance_semiring_laws_hold: summaries
802            .iter()
803            .all(|summary| summary.capabilities.linear_provenance_semiring_laws_hold),
804    }
805}
806
807#[derive(Debug, Clone)]
808struct OmenaQueryCrossFileSummaryEdgeInput {
809    edge_kind: &'static str,
810    from_kind: &'static str,
811    target_kind: Option<&'static str>,
812    from_path: String,
813    target_path: Option<String>,
814    source: Option<String>,
815    owner_selector_name: Option<String>,
816    local_name: Option<String>,
817    remote_name: Option<String>,
818    target_names: Vec<String>,
819    status: &'static str,
820    provenance: Vec<&'static str>,
821}
822
823fn build_omena_query_cross_file_summary_edge(
824    input: OmenaQueryCrossFileSummaryEdgeInput,
825) -> OmenaQueryCrossFileSummaryEdgeV0 {
826    let edge_id = omena_query_cross_file_summary_edge_id(&input);
827    let linear_provenance = summarize_omena_query_linear_provenance_with_support_count(
828        input.provenance.as_slice(),
829        linear_provenance_support_count(&input),
830    );
831
832    OmenaQueryCrossFileSummaryEdgeV0 {
833        edge_id,
834        edge_kind: input.edge_kind,
835        from_kind: input.from_kind,
836        from_path: input.from_path,
837        target_kind: input.target_kind,
838        target_path: input.target_path,
839        source: input.source,
840        owner_selector_name: input.owner_selector_name,
841        local_name: input.local_name,
842        remote_name: input.remote_name,
843        target_names: input.target_names,
844        status: input.status,
845        provenance: input.provenance,
846        linear_provenance,
847    }
848}
849
850fn linear_provenance_support_count(input: &OmenaQueryCrossFileSummaryEdgeInput) -> u8 {
851    if !cross_file_summary_status_has_supported_target(input.status) {
852        return 0;
853    }
854    input.target_names.len().max(1).min(usize::from(u8::MAX)) as u8
855}
856
857fn cross_file_summary_status_has_supported_target(status: &str) -> bool {
858    matches!(
859        status,
860        "resolved" | "reachable" | "localResolved" | "importResolved" | "external"
861    )
862}
863
864fn omena_query_cross_file_summary_edge_id(input: &OmenaQueryCrossFileSummaryEdgeInput) -> String {
865    format!(
866        "{}|fromKind:{}|from:{}|targetKind:{}|target:{}|source:{}|owner:{}|local:{}|remote:{}|names:{}",
867        input.edge_kind,
868        input.from_kind,
869        input.from_path,
870        input.target_kind.unwrap_or("-"),
871        input.target_path.as_deref().unwrap_or("-"),
872        input.source.as_deref().unwrap_or("-"),
873        input.owner_selector_name.as_deref().unwrap_or("-"),
874        input.local_name.as_deref().unwrap_or("-"),
875        input.remote_name.as_deref().unwrap_or("-"),
876        input.target_names.join(",")
877    )
878}
879
880fn stable_omena_query_cross_file_summary_hash(
881    edges: &[OmenaQueryCrossFileSummaryEdgeV0],
882) -> String {
883    let mut hash = 0xcbf29ce484222325u64;
884    stable_omena_query_hash_piece(&mut hash, "omena-query.cross-file-summary");
885    stable_omena_query_hash_piece(&mut hash, "0");
886    for edge in edges {
887        stable_omena_query_hash_piece(&mut hash, edge.edge_id.as_str());
888        stable_omena_query_hash_piece(&mut hash, edge.status);
889        stable_omena_query_hash_piece(&mut hash, edge.linear_provenance.semiring_identifier());
890        let term_count = edge.linear_provenance.term_count.to_string();
891        stable_omena_query_hash_piece(&mut hash, term_count.as_str());
892        for term in &edge.linear_provenance.terms {
893            let coefficient = term.coefficient.to_string();
894            stable_omena_query_hash_piece(&mut hash, coefficient.as_str());
895            stable_omena_query_hash_piece(&mut hash, term.label);
896        }
897    }
898    format!("{hash:016x}")
899}
900
901impl OmenaQueryCrossFileSummaryV0 {
902    pub fn recompute_stable_summary_hash(&self) -> String {
903        stable_omena_query_cross_file_summary_hash(self.edges.as_slice())
904    }
905}
906
907fn summarize_omena_query_cross_file_summary_edge_kind_counts(
908    edges: &[OmenaQueryCrossFileSummaryEdgeV0],
909) -> Vec<OmenaQueryCrossFileSummaryEdgeKindCountV0> {
910    let mut counts = BTreeMap::<&'static str, usize>::new();
911    for edge in edges {
912        *counts.entry(edge.edge_kind).or_default() += 1;
913    }
914    counts
915        .into_iter()
916        .map(|(edge_kind, count)| OmenaQueryCrossFileSummaryEdgeKindCountV0 { edge_kind, count })
917        .collect()
918}
919
920fn stable_omena_query_hash_piece(hash: &mut u64, piece: &str) {
921    for byte in piece.as_bytes() {
922        *hash ^= u64::from(*byte);
923        *hash = hash.wrapping_mul(0x100000001b3);
924    }
925    *hash ^= 0xff;
926    *hash = hash.wrapping_mul(0x100000001b3);
927}