Skip to main content

omena_cross_file_summary/
lib.rs

1//! Cross-file summary hypergraph substrate shared by query and streaming IFDS.
2
3use std::collections::{BTreeMap, BTreeSet, VecDeque};
4
5use omena_abstract_value::{LinearProvenanceV0, NaturalCountProvenanceSemiringV0};
6use serde::Serialize;
7
8pub type OmenaCrossFileLinearProvenanceV0 = LinearProvenanceV0<NaturalCountProvenanceSemiringV0>;
9
10#[derive(Debug, Clone, PartialEq, Eq, Serialize)]
11#[serde(rename_all = "camelCase")]
12pub struct OmenaQueryCrossFileSummaryV0 {
13    pub schema_version: &'static str,
14    pub product: &'static str,
15    pub status: &'static str,
16    pub summary_scope: &'static str,
17    pub style_count: usize,
18    pub summary_edge_count: usize,
19    pub edge_kind_counts: Vec<OmenaQueryCrossFileSummaryEdgeKindCountV0>,
20    pub summary_hash: String,
21    pub edges: Vec<OmenaQueryCrossFileSummaryEdgeV0>,
22    pub capabilities: OmenaQueryCrossFileSummaryCapabilitiesV0,
23    pub next_priorities: Vec<&'static str>,
24}
25
26#[derive(Debug, Clone, PartialEq, Eq, Serialize)]
27#[serde(rename_all = "camelCase")]
28pub struct OmenaQueryCrossFileSummaryEdgeKindCountV0 {
29    pub edge_kind: &'static str,
30    pub count: usize,
31}
32
33#[derive(Debug, Clone, PartialEq, Eq, Serialize)]
34#[serde(rename_all = "camelCase")]
35pub struct OmenaCrossFileSummaryViewReportV0 {
36    pub schema_version: &'static str,
37    pub product: &'static str,
38    pub status: &'static str,
39    pub raw_edge_kind_catalog_count: usize,
40    pub node_role_catalog_count: usize,
41    pub summary_edge_count: usize,
42    pub existing_edge_kind_counts: Vec<OmenaQueryCrossFileSummaryEdgeKindCountV0>,
43    pub recomputed_edge_kind_counts: Vec<OmenaQueryCrossFileSummaryEdgeKindCountV0>,
44    pub invalid_raw_edge_kinds: Vec<String>,
45    pub invalid_node_roles: Vec<String>,
46    pub all_raw_edge_kinds_in_catalog: bool,
47    pub all_node_roles_in_catalog: bool,
48    pub edge_kind_counts_match_existing_field: bool,
49    pub summary_view_ready: bool,
50}
51
52#[derive(Debug, Clone, PartialEq, Eq, Serialize)]
53#[serde(rename_all = "camelCase")]
54pub struct OmenaCrossFileGraphDeltaV0 {
55    pub schema_version: &'static str,
56    pub product: &'static str,
57    pub status: &'static str,
58    pub before_edge_count: usize,
59    pub after_edge_count: usize,
60    pub added_edges: Vec<OmenaCrossFileGraphDeltaEdgeV0>,
61    pub removed_edges: Vec<OmenaCrossFileGraphDeltaEdgeV0>,
62    pub invalid_delta_edge_ids: Vec<String>,
63    pub all_delta_edges_typed: bool,
64}
65
66#[derive(Debug, Clone, PartialEq, Eq, Serialize)]
67#[serde(rename_all = "camelCase")]
68pub struct OmenaCrossFileGraphDeltaEdgeV0 {
69    pub edge_id: String,
70    pub raw_edge_kind: &'static str,
71    pub folded_edge_kind: UnifiedHypergraphEdgeKindV0,
72    pub folded_by_lossy_catch_all: bool,
73    pub from_role: OmenaCrossFileSummaryNodeRoleV0,
74    pub target_role: Option<OmenaCrossFileSummaryNodeRoleV0>,
75    pub from_path: String,
76    pub target_path: Option<String>,
77}
78
79#[derive(Debug, Clone, PartialEq, Eq, Serialize)]
80#[serde(rename_all = "camelCase")]
81pub struct OmenaQueryCrossFileSummaryEdgeV0 {
82    pub edge_id: String,
83    pub edge_kind: &'static str,
84    pub from_kind: &'static str,
85    pub from_path: String,
86    pub target_kind: Option<&'static str>,
87    pub target_path: Option<String>,
88    pub source: Option<String>,
89    pub owner_selector_name: Option<String>,
90    pub local_name: Option<String>,
91    pub remote_name: Option<String>,
92    pub target_names: Vec<String>,
93    pub status: &'static str,
94    pub provenance: Vec<&'static str>,
95    pub linear_provenance: OmenaCrossFileLinearProvenanceV0,
96}
97
98#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Hash, Serialize)]
99#[serde(rename_all = "camelCase")]
100pub enum OmenaCrossFileSummaryNodeRoleV0 {
101    Source,
102    Style,
103}
104
105impl OmenaCrossFileSummaryNodeRoleV0 {
106    pub const fn as_wire_label(self) -> &'static str {
107        match self {
108            Self::Source => "source",
109            Self::Style => "style",
110        }
111    }
112}
113
114#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Hash, Serialize)]
115#[serde(rename_all = "camelCase")]
116pub enum OmenaCrossFileSummaryRawEdgeKindV0 {
117    ComposesExternal,
118    ComposesGlobal,
119    ComposesLocal,
120    CssModulesComposesClosure,
121    CssModulesComposesImport,
122    CssModulesIcssClosure,
123    CssModulesIcssImport,
124    CssModulesImport,
125    CssModulesValueClosure,
126    CssModulesValueImport,
127    ForeignReference,
128    Icss,
129    LessImport,
130    LessModuleGraphClosure,
131    SassForward,
132    SassImport,
133    SassModuleGraphClosure,
134    SassUse,
135    SourceSelectorPrefixReference,
136    SourceSelectorReference,
137    StyleDesignTokenReference,
138    Value,
139}
140
141impl OmenaCrossFileSummaryRawEdgeKindV0 {
142    pub const fn as_wire_label(self) -> &'static str {
143        match self {
144            Self::ComposesExternal => "composesExternal",
145            Self::ComposesGlobal => "composesGlobal",
146            Self::ComposesLocal => "composesLocal",
147            Self::CssModulesComposesClosure => "cssModulesComposesClosure",
148            Self::CssModulesComposesImport => "cssModulesComposesImport",
149            Self::CssModulesIcssClosure => "cssModulesIcssClosure",
150            Self::CssModulesIcssImport => "cssModulesIcssImport",
151            Self::CssModulesImport => "cssModulesImport",
152            Self::CssModulesValueClosure => "cssModulesValueClosure",
153            Self::CssModulesValueImport => "cssModulesValueImport",
154            Self::ForeignReference => "foreignReference",
155            Self::Icss => "icss",
156            Self::LessImport => "lessImport",
157            Self::LessModuleGraphClosure => "lessModuleGraphClosure",
158            Self::SassForward => "sassForward",
159            Self::SassImport => "sassImport",
160            Self::SassModuleGraphClosure => "sassModuleGraphClosure",
161            Self::SassUse => "sassUse",
162            Self::SourceSelectorPrefixReference => "sourceSelectorPrefixReference",
163            Self::SourceSelectorReference => "sourceSelectorReference",
164            Self::StyleDesignTokenReference => "styleDesignTokenReference",
165            Self::Value => "value",
166        }
167    }
168
169    pub const fn folded_edge_kind(self) -> UnifiedHypergraphEdgeKindV0 {
170        match self {
171            Self::ComposesLocal => UnifiedHypergraphEdgeKindV0::ComposesLocal,
172            Self::ComposesGlobal => UnifiedHypergraphEdgeKindV0::ComposesGlobal,
173            Self::CssModulesComposesImport
174            | Self::CssModulesComposesClosure
175            | Self::ComposesExternal => UnifiedHypergraphEdgeKindV0::ComposesExternal,
176            Self::SassUse => UnifiedHypergraphEdgeKindV0::SassUse,
177            Self::SassForward => UnifiedHypergraphEdgeKindV0::SassForward,
178            Self::SassImport => UnifiedHypergraphEdgeKindV0::SassImport,
179            Self::LessImport => UnifiedHypergraphEdgeKindV0::LessImport,
180            Self::LessModuleGraphClosure => UnifiedHypergraphEdgeKindV0::LessModuleGraphClosure,
181            Self::CssModulesValueImport | Self::CssModulesValueClosure | Self::Value => {
182                UnifiedHypergraphEdgeKindV0::Value
183            }
184            Self::CssModulesIcssImport | Self::CssModulesIcssClosure | Self::Icss => {
185                UnifiedHypergraphEdgeKindV0::Icss
186            }
187            Self::CssModulesImport
188            | Self::ForeignReference
189            | Self::SassModuleGraphClosure
190            | Self::SourceSelectorPrefixReference
191            | Self::SourceSelectorReference
192            | Self::StyleDesignTokenReference => UnifiedHypergraphEdgeKindV0::ForeignReference,
193        }
194    }
195
196    pub const fn folded_by_lossy_catch_all(self) -> bool {
197        matches!(
198            self,
199            Self::CssModulesImport
200                | Self::ForeignReference
201                | Self::SassModuleGraphClosure
202                | Self::SourceSelectorPrefixReference
203                | Self::SourceSelectorReference
204                | Self::StyleDesignTokenReference
205        )
206    }
207
208    pub const fn order_relevance(self) -> EdgeOrderRelevanceV0 {
209        match self {
210            Self::CssModulesImport => EdgeOrderRelevanceV0::OrderBearing,
211            Self::ForeignReference
212            | Self::SassModuleGraphClosure
213            | Self::SourceSelectorPrefixReference
214            | Self::SourceSelectorReference
215            | Self::StyleDesignTokenReference => EdgeOrderRelevanceV0::OrderNeutral,
216            _ => self.folded_edge_kind().order_relevance(),
217        }
218    }
219}
220
221pub const UNIFIED_HYPERGRAPH_EDGE_KIND_VARIANTS_V0: [UnifiedHypergraphEdgeKindV0; 11] = [
222    UnifiedHypergraphEdgeKindV0::ComposesLocal,
223    UnifiedHypergraphEdgeKindV0::ComposesGlobal,
224    UnifiedHypergraphEdgeKindV0::ComposesExternal,
225    UnifiedHypergraphEdgeKindV0::SassUse,
226    UnifiedHypergraphEdgeKindV0::SassForward,
227    UnifiedHypergraphEdgeKindV0::SassImport,
228    UnifiedHypergraphEdgeKindV0::LessImport,
229    UnifiedHypergraphEdgeKindV0::LessModuleGraphClosure,
230    UnifiedHypergraphEdgeKindV0::Value,
231    UnifiedHypergraphEdgeKindV0::Icss,
232    UnifiedHypergraphEdgeKindV0::ForeignReference,
233];
234
235pub const CROSS_FILE_SUMMARY_RAW_EDGE_KIND_VARIANTS_V0: [OmenaCrossFileSummaryRawEdgeKindV0; 22] = [
236    OmenaCrossFileSummaryRawEdgeKindV0::ComposesExternal,
237    OmenaCrossFileSummaryRawEdgeKindV0::ComposesGlobal,
238    OmenaCrossFileSummaryRawEdgeKindV0::ComposesLocal,
239    OmenaCrossFileSummaryRawEdgeKindV0::CssModulesComposesClosure,
240    OmenaCrossFileSummaryRawEdgeKindV0::CssModulesComposesImport,
241    OmenaCrossFileSummaryRawEdgeKindV0::CssModulesIcssClosure,
242    OmenaCrossFileSummaryRawEdgeKindV0::CssModulesIcssImport,
243    OmenaCrossFileSummaryRawEdgeKindV0::CssModulesImport,
244    OmenaCrossFileSummaryRawEdgeKindV0::CssModulesValueClosure,
245    OmenaCrossFileSummaryRawEdgeKindV0::CssModulesValueImport,
246    OmenaCrossFileSummaryRawEdgeKindV0::ForeignReference,
247    OmenaCrossFileSummaryRawEdgeKindV0::Icss,
248    OmenaCrossFileSummaryRawEdgeKindV0::LessImport,
249    OmenaCrossFileSummaryRawEdgeKindV0::LessModuleGraphClosure,
250    OmenaCrossFileSummaryRawEdgeKindV0::SassForward,
251    OmenaCrossFileSummaryRawEdgeKindV0::SassImport,
252    OmenaCrossFileSummaryRawEdgeKindV0::SassModuleGraphClosure,
253    OmenaCrossFileSummaryRawEdgeKindV0::SassUse,
254    OmenaCrossFileSummaryRawEdgeKindV0::SourceSelectorPrefixReference,
255    OmenaCrossFileSummaryRawEdgeKindV0::SourceSelectorReference,
256    OmenaCrossFileSummaryRawEdgeKindV0::StyleDesignTokenReference,
257    OmenaCrossFileSummaryRawEdgeKindV0::Value,
258];
259
260pub const CROSS_FILE_SUMMARY_RAW_EDGE_KIND_LABELS_V0: [&str; 22] = [
261    "composesExternal",
262    "composesGlobal",
263    "composesLocal",
264    "cssModulesComposesClosure",
265    "cssModulesComposesImport",
266    "cssModulesIcssClosure",
267    "cssModulesIcssImport",
268    "cssModulesImport",
269    "cssModulesValueClosure",
270    "cssModulesValueImport",
271    "foreignReference",
272    "icss",
273    "lessImport",
274    "lessModuleGraphClosure",
275    "sassForward",
276    "sassImport",
277    "sassModuleGraphClosure",
278    "sassUse",
279    "sourceSelectorPrefixReference",
280    "sourceSelectorReference",
281    "styleDesignTokenReference",
282    "value",
283];
284
285pub const CROSS_FILE_SUMMARY_NODE_ROLE_LABELS_V0: [&str; 2] = ["source", "style"];
286
287pub fn parse_cross_file_summary_node_role_v0(
288    label: &str,
289) -> Option<OmenaCrossFileSummaryNodeRoleV0> {
290    match label {
291        "source" => Some(OmenaCrossFileSummaryNodeRoleV0::Source),
292        "style" => Some(OmenaCrossFileSummaryNodeRoleV0::Style),
293        _ => None,
294    }
295}
296
297pub fn parse_cross_file_summary_raw_edge_kind_v0(
298    label: &str,
299) -> Option<OmenaCrossFileSummaryRawEdgeKindV0> {
300    match label {
301        "composesExternal" => Some(OmenaCrossFileSummaryRawEdgeKindV0::ComposesExternal),
302        "composesGlobal" => Some(OmenaCrossFileSummaryRawEdgeKindV0::ComposesGlobal),
303        "composesLocal" => Some(OmenaCrossFileSummaryRawEdgeKindV0::ComposesLocal),
304        "cssModulesComposesClosure" => {
305            Some(OmenaCrossFileSummaryRawEdgeKindV0::CssModulesComposesClosure)
306        }
307        "cssModulesComposesImport" => {
308            Some(OmenaCrossFileSummaryRawEdgeKindV0::CssModulesComposesImport)
309        }
310        "cssModulesIcssClosure" => Some(OmenaCrossFileSummaryRawEdgeKindV0::CssModulesIcssClosure),
311        "cssModulesIcssImport" => Some(OmenaCrossFileSummaryRawEdgeKindV0::CssModulesIcssImport),
312        "cssModulesImport" => Some(OmenaCrossFileSummaryRawEdgeKindV0::CssModulesImport),
313        "cssModulesValueClosure" => {
314            Some(OmenaCrossFileSummaryRawEdgeKindV0::CssModulesValueClosure)
315        }
316        "cssModulesValueImport" => Some(OmenaCrossFileSummaryRawEdgeKindV0::CssModulesValueImport),
317        "foreignReference" => Some(OmenaCrossFileSummaryRawEdgeKindV0::ForeignReference),
318        "icss" => Some(OmenaCrossFileSummaryRawEdgeKindV0::Icss),
319        "lessImport" => Some(OmenaCrossFileSummaryRawEdgeKindV0::LessImport),
320        "lessModuleGraphClosure" => {
321            Some(OmenaCrossFileSummaryRawEdgeKindV0::LessModuleGraphClosure)
322        }
323        "sassForward" => Some(OmenaCrossFileSummaryRawEdgeKindV0::SassForward),
324        "sassImport" => Some(OmenaCrossFileSummaryRawEdgeKindV0::SassImport),
325        "sassModuleGraphClosure" => {
326            Some(OmenaCrossFileSummaryRawEdgeKindV0::SassModuleGraphClosure)
327        }
328        "sassUse" => Some(OmenaCrossFileSummaryRawEdgeKindV0::SassUse),
329        "sourceSelectorPrefixReference" => {
330            Some(OmenaCrossFileSummaryRawEdgeKindV0::SourceSelectorPrefixReference)
331        }
332        "sourceSelectorReference" => {
333            Some(OmenaCrossFileSummaryRawEdgeKindV0::SourceSelectorReference)
334        }
335        "styleDesignTokenReference" => {
336            Some(OmenaCrossFileSummaryRawEdgeKindV0::StyleDesignTokenReference)
337        }
338        "value" => Some(OmenaCrossFileSummaryRawEdgeKindV0::Value),
339        _ => None,
340    }
341}
342
343#[derive(Debug, Clone, PartialEq, Eq, Serialize)]
344#[serde(rename_all = "camelCase")]
345pub struct OmenaQueryCrossFileSummaryCapabilitiesV0 {
346    pub css_modules_composes_edges_ready: bool,
347    pub css_modules_value_edges_ready: bool,
348    pub css_modules_icss_edges_ready: bool,
349    pub sass_module_edges_ready: bool,
350    pub style_design_token_reference_edges_ready: bool,
351    pub source_selector_reference_edges_ready: bool,
352    pub stable_summary_hash_ready: bool,
353    pub linear_provenance_ready: bool,
354    pub linear_provenance_round_trip_ready: bool,
355    pub linear_provenance_semiring_laws_hold: bool,
356}
357
358#[derive(Debug, Clone, Default, PartialEq, Eq, Serialize)]
359#[serde(rename_all = "camelCase")]
360pub struct ReverseDependencyIndexV0 {
361    pub rev: BTreeMap<String, BTreeSet<String>>,
362    pub edges_by_from: BTreeMap<String, BTreeSet<String>>,
363}
364
365impl OmenaQueryCrossFileSummaryEdgeV0 {
366    pub fn linear_provenance_round_trips_legacy_labels(&self) -> bool {
367        self.linear_provenance.labels() == self.provenance
368    }
369}
370
371impl OmenaQueryCrossFileSummaryV0 {
372    pub fn linear_provenance_round_trips_legacy_labels(&self) -> bool {
373        self.edges
374            .iter()
375            .all(OmenaQueryCrossFileSummaryEdgeV0::linear_provenance_round_trips_legacy_labels)
376    }
377
378    pub fn recompute_stable_summary_hash(&self) -> String {
379        stable_omena_query_cross_file_summary_hash(self.edges.as_slice())
380    }
381}
382
383pub fn summarize_cross_file_summary_view_v0(
384    summary: &OmenaQueryCrossFileSummaryV0,
385) -> OmenaCrossFileSummaryViewReportV0 {
386    let recomputed_edge_kind_counts =
387        recompute_cross_file_summary_raw_edge_kind_counts_v0(summary.edges.as_slice());
388    let invalid_raw_edge_kinds = invalid_raw_edge_kinds_for_summary(summary);
389    let invalid_node_roles = invalid_node_roles_for_summary(summary);
390    let edge_kind_counts_match_existing_field =
391        recomputed_edge_kind_counts == summary.edge_kind_counts;
392    let all_raw_edge_kinds_in_catalog = invalid_raw_edge_kinds.is_empty();
393    let all_node_roles_in_catalog = invalid_node_roles.is_empty();
394    let summary_view_ready = all_raw_edge_kinds_in_catalog
395        && all_node_roles_in_catalog
396        && edge_kind_counts_match_existing_field;
397
398    OmenaCrossFileSummaryViewReportV0 {
399        schema_version: "0",
400        product: "omena-cross-file-summary.summary-view",
401        status: if summary_view_ready {
402            "ready"
403        } else {
404            "needsInput"
405        },
406        raw_edge_kind_catalog_count: CROSS_FILE_SUMMARY_RAW_EDGE_KIND_LABELS_V0.len(),
407        node_role_catalog_count: CROSS_FILE_SUMMARY_NODE_ROLE_LABELS_V0.len(),
408        summary_edge_count: summary.summary_edge_count,
409        existing_edge_kind_counts: summary.edge_kind_counts.clone(),
410        recomputed_edge_kind_counts,
411        invalid_raw_edge_kinds,
412        invalid_node_roles,
413        all_raw_edge_kinds_in_catalog,
414        all_node_roles_in_catalog,
415        edge_kind_counts_match_existing_field,
416        summary_view_ready,
417    }
418}
419
420pub fn recompute_cross_file_summary_raw_edge_kind_counts_v0(
421    edges: &[OmenaQueryCrossFileSummaryEdgeV0],
422) -> Vec<OmenaQueryCrossFileSummaryEdgeKindCountV0> {
423    let mut counts = BTreeMap::<&'static str, usize>::new();
424    for edge in edges {
425        *counts.entry(edge.edge_kind).or_default() += 1;
426    }
427    counts
428        .into_iter()
429        .map(|(edge_kind, count)| OmenaQueryCrossFileSummaryEdgeKindCountV0 { edge_kind, count })
430        .collect()
431}
432
433pub fn summarize_cross_file_graph_delta_v0(
434    before: &OmenaQueryCrossFileSummaryV0,
435    after: &OmenaQueryCrossFileSummaryV0,
436) -> OmenaCrossFileGraphDeltaV0 {
437    let before_edges = before
438        .edges
439        .iter()
440        .map(|edge| (edge.edge_id.as_str(), edge))
441        .collect::<BTreeMap<_, _>>();
442    let after_edges = after
443        .edges
444        .iter()
445        .map(|edge| (edge.edge_id.as_str(), edge))
446        .collect::<BTreeMap<_, _>>();
447
448    let mut invalid_delta_edge_ids = Vec::new();
449    let mut added_edges = Vec::new();
450    let mut removed_edges = Vec::new();
451
452    for (edge_id, edge) in &after_edges {
453        if before_edges.contains_key(edge_id) {
454            continue;
455        }
456        if let Some(typed_edge) = typed_graph_delta_edge_from_summary_edge(edge) {
457            added_edges.push(typed_edge);
458        } else {
459            invalid_delta_edge_ids.push((*edge_id).to_string());
460        }
461    }
462    for (edge_id, edge) in &before_edges {
463        if after_edges.contains_key(edge_id) {
464            continue;
465        }
466        if let Some(typed_edge) = typed_graph_delta_edge_from_summary_edge(edge) {
467            removed_edges.push(typed_edge);
468        } else {
469            invalid_delta_edge_ids.push((*edge_id).to_string());
470        }
471    }
472
473    added_edges.sort_by_key(|edge| edge.edge_id.clone());
474    removed_edges.sort_by_key(|edge| edge.edge_id.clone());
475    invalid_delta_edge_ids.sort();
476    let all_delta_edges_typed = invalid_delta_edge_ids.is_empty();
477
478    OmenaCrossFileGraphDeltaV0 {
479        schema_version: "0",
480        product: "omena-cross-file-summary.graph-delta",
481        status: if all_delta_edges_typed {
482            "ready"
483        } else {
484            "needsInput"
485        },
486        before_edge_count: before.summary_edge_count,
487        after_edge_count: after.summary_edge_count,
488        added_edges,
489        removed_edges,
490        invalid_delta_edge_ids,
491        all_delta_edges_typed,
492    }
493}
494
495fn invalid_raw_edge_kinds_for_summary(summary: &OmenaQueryCrossFileSummaryV0) -> Vec<String> {
496    summary
497        .edges
498        .iter()
499        .filter(|edge| parse_cross_file_summary_raw_edge_kind_v0(edge.edge_kind).is_none())
500        .map(|edge| edge.edge_kind.to_string())
501        .collect::<BTreeSet<_>>()
502        .into_iter()
503        .collect()
504}
505
506fn invalid_node_roles_for_summary(summary: &OmenaQueryCrossFileSummaryV0) -> Vec<String> {
507    let mut invalid = BTreeSet::new();
508    for edge in &summary.edges {
509        if parse_cross_file_summary_node_role_v0(edge.from_kind).is_none() {
510            invalid.insert(edge.from_kind.to_string());
511        }
512        if let Some(target_kind) = edge.target_kind
513            && parse_cross_file_summary_node_role_v0(target_kind).is_none()
514        {
515            invalid.insert(target_kind.to_string());
516        }
517    }
518    invalid.into_iter().collect()
519}
520
521fn typed_graph_delta_edge_from_summary_edge(
522    edge: &OmenaQueryCrossFileSummaryEdgeV0,
523) -> Option<OmenaCrossFileGraphDeltaEdgeV0> {
524    let raw_edge_kind = parse_cross_file_summary_raw_edge_kind_v0(edge.edge_kind)?;
525    let from_role = parse_cross_file_summary_node_role_v0(edge.from_kind)?;
526    let target_role = match edge.target_kind {
527        Some(target_kind) => Some(parse_cross_file_summary_node_role_v0(target_kind)?),
528        None => None,
529    };
530    Some(OmenaCrossFileGraphDeltaEdgeV0 {
531        edge_id: edge.edge_id.clone(),
532        raw_edge_kind: raw_edge_kind.as_wire_label(),
533        folded_edge_kind: raw_edge_kind.folded_edge_kind(),
534        folded_by_lossy_catch_all: raw_edge_kind.folded_by_lossy_catch_all(),
535        from_role,
536        target_role,
537        from_path: edge.from_path.clone(),
538        target_path: edge.target_path.clone(),
539    })
540}
541
542pub fn reverse_dependency_index_from_edges_v0(
543    edges: &[OmenaQueryCrossFileSummaryEdgeV0],
544) -> ReverseDependencyIndexV0 {
545    let mut index = ReverseDependencyIndexV0 {
546        rev: BTreeMap::new(),
547        edges_by_from: BTreeMap::new(),
548    };
549    for edge in edges {
550        let Some(target_path) = edge.target_path.as_ref() else {
551            continue;
552        };
553        index
554            .edges_by_from
555            .entry(edge.from_path.clone())
556            .or_default()
557            .insert(target_path.clone());
558        index
559            .rev
560            .entry(target_path.clone())
561            .or_default()
562            .insert(edge.from_path.clone());
563    }
564    index
565}
566
567pub fn apply_reverse_dependency_delta_v0(
568    index: &mut ReverseDependencyIndexV0,
569    next_edges: &[OmenaQueryCrossFileSummaryEdgeV0],
570) -> usize {
571    let next_groups = reverse_dependency_groups_by_from_path(next_edges);
572    let mut from_paths = index.edges_by_from.keys().cloned().collect::<BTreeSet<_>>();
573    from_paths.extend(next_groups.keys().cloned());
574
575    let mut patched_groups = 0;
576    for from_path in from_paths {
577        let old_targets = index
578            .edges_by_from
579            .get(from_path.as_str())
580            .cloned()
581            .unwrap_or_default();
582        let new_targets = next_groups
583            .get(from_path.as_str())
584            .cloned()
585            .unwrap_or_default();
586        if old_targets == new_targets {
587            continue;
588        }
589        patched_groups += 1;
590        for target in &old_targets {
591            if let Some(dependents) = index.rev.get_mut(target.as_str()) {
592                dependents.remove(from_path.as_str());
593                if dependents.is_empty() {
594                    index.rev.remove(target.as_str());
595                }
596            }
597        }
598        if new_targets.is_empty() {
599            index.edges_by_from.remove(from_path.as_str());
600        } else {
601            for target in &new_targets {
602                index
603                    .rev
604                    .entry(target.clone())
605                    .or_default()
606                    .insert(from_path.clone());
607            }
608            index.edges_by_from.insert(from_path, new_targets);
609        }
610    }
611    patched_groups
612}
613
614pub fn reverse_dependency_closure_v0(
615    index: &ReverseDependencyIndexV0,
616    seeds: &BTreeSet<String>,
617) -> BTreeSet<String> {
618    let mut visited = BTreeSet::new();
619    let mut queue = seeds.iter().cloned().collect::<VecDeque<_>>();
620    while let Some(path) = queue.pop_front() {
621        if let Some(dependents) = index.rev.get(path.as_str()) {
622            for dependent in dependents {
623                if visited.insert(dependent.clone()) {
624                    queue.push_back(dependent.clone());
625                }
626            }
627        }
628    }
629    visited
630}
631
632/// The declared read-set of ONE style target's workspace diagnostics, as a
633/// path set over the summary's edge graph: the target itself, its FORWARD
634/// closure (modules whose contents the target's resolution, token, cascade,
635/// replica-ensemble, and reachability arms read), and its DIRECT reverse
636/// neighbors (style and source files whose references decide the target's
637/// usage-derived diagnostics). This is the verifying-trace manifest for the
638/// disk diagnostics cache: completeness is oracle-gated in the LSP crate
639/// (a manifest-verified hit must be byte-identical to a fresh compute), so
640/// any diagnostics arm that grows a wider read window fails the oracle
641/// instead of silently serving stale shards.
642pub fn diagnostics_read_set_for_target_v0(
643    index: &ReverseDependencyIndexV0,
644    target_path: &str,
645) -> BTreeSet<String> {
646    let mut read_set = BTreeSet::from([target_path.to_string()]);
647    let mut queue = VecDeque::from([target_path.to_string()]);
648    while let Some(path) = queue.pop_front() {
649        if let Some(targets) = index.edges_by_from.get(path.as_str()) {
650            for next in targets {
651                if read_set.insert(next.clone()) {
652                    queue.push_back(next.clone());
653                }
654            }
655        }
656    }
657    if let Some(dependents) = index.rev.get(target_path) {
658        read_set.extend(dependents.iter().cloned());
659    }
660    read_set
661}
662
663fn reverse_dependency_groups_by_from_path(
664    edges: &[OmenaQueryCrossFileSummaryEdgeV0],
665) -> BTreeMap<String, BTreeSet<String>> {
666    let mut groups = BTreeMap::<String, BTreeSet<String>>::new();
667    for edge in edges {
668        let Some(target_path) = edge.target_path.as_ref() else {
669            continue;
670        };
671        groups
672            .entry(edge.from_path.clone())
673            .or_default()
674            .insert(target_path.clone());
675    }
676    groups
677}
678
679#[non_exhaustive]
680#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Hash, Serialize)]
681#[serde(rename_all = "camelCase")]
682pub enum UnifiedHypergraphEdgeKindV0 {
683    ComposesLocal,
684    ComposesGlobal,
685    ComposesExternal,
686    SassUse,
687    SassForward,
688    SassImport,
689    LessImport,
690    LessModuleGraphClosure,
691    Value,
692    Icss,
693    ForeignReference,
694}
695
696#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize)]
697#[serde(rename_all = "camelCase")]
698pub enum EdgeOrderRelevanceV0 {
699    OrderBearing,
700    OrderNeutral,
701}
702
703impl EdgeOrderRelevanceV0 {
704    pub const fn as_wire_label(self) -> &'static str {
705        match self {
706            Self::OrderBearing => "orderBearing",
707            Self::OrderNeutral => "orderNeutral",
708        }
709    }
710}
711
712impl UnifiedHypergraphEdgeKindV0 {
713    pub const fn as_wire_label(self) -> &'static str {
714        match self {
715            Self::ComposesLocal => "composesLocal",
716            Self::ComposesGlobal => "composesGlobal",
717            Self::ComposesExternal => "composesExternal",
718            Self::SassUse => "sassUse",
719            Self::SassForward => "sassForward",
720            Self::SassImport => "sassImport",
721            Self::LessImport => "lessImport",
722            Self::LessModuleGraphClosure => "lessModuleGraphClosure",
723            Self::Value => "value",
724            Self::Icss => "icss",
725            Self::ForeignReference => "foreignReference",
726        }
727    }
728
729    pub const fn order_relevance(self) -> EdgeOrderRelevanceV0 {
730        match self {
731            Self::ComposesLocal
732            | Self::ComposesGlobal
733            | Self::ComposesExternal
734            | Self::SassUse
735            | Self::SassForward
736            | Self::SassImport
737            | Self::LessImport
738            | Self::Value
739            | Self::Icss => EdgeOrderRelevanceV0::OrderBearing,
740            Self::LessModuleGraphClosure | Self::ForeignReference => {
741                EdgeOrderRelevanceV0::OrderNeutral
742            }
743        }
744    }
745
746    pub const fn is_order_significant(self) -> bool {
747        matches!(self.order_relevance(), EdgeOrderRelevanceV0::OrderBearing)
748    }
749}
750
751#[derive(Debug, Clone, PartialEq, Eq, Serialize)]
752#[serde(rename_all = "camelCase")]
753pub struct UnifiedHypergraphHyperedgeV0 {
754    pub schema_version: &'static str,
755    pub product: &'static str,
756    pub layer_marker: &'static str,
757    pub feature_gate: &'static str,
758    pub hyperedge_id: String,
759    pub edge_kind: UnifiedHypergraphEdgeKindV0,
760    pub source_summary_edge_id: String,
761    pub source_edge_kind: &'static str,
762    pub source_status: &'static str,
763    pub tail_node_ids: Vec<String>,
764    pub head_node_id: String,
765    pub order_significant_tail: bool,
766}
767
768#[derive(Debug, Clone, PartialEq, Eq, Serialize)]
769#[serde(rename_all = "camelCase")]
770pub struct HypergraphIFDSSummaryEdgeV0 {
771    pub schema_version: &'static str,
772    pub product: &'static str,
773    pub layer_marker: &'static str,
774    pub feature_gate: &'static str,
775    pub summary_edge_id: String,
776    pub projection_edge_id: String,
777    pub hyperedge_id: String,
778    pub from_node_id: String,
779    pub to_node_id: String,
780    pub edge_kind: UnifiedHypergraphEdgeKindV0,
781    pub status: &'static str,
782    pub provenance: Vec<&'static str>,
783    pub linear_provenance: OmenaCrossFileLinearProvenanceV0,
784}
785
786#[derive(Debug, Clone, PartialEq, Eq, Serialize)]
787#[serde(rename_all = "camelCase")]
788pub struct OmenaQueryUnifiedCrossFileHypergraphV0 {
789    pub schema_version: &'static str,
790    pub product: &'static str,
791    pub status: &'static str,
792    pub layer_marker: &'static str,
793    pub feature_gate: &'static str,
794    pub node_count: usize,
795    pub hyperedge_count: usize,
796    pub summary_edge_count: usize,
797    pub projection_edge_ids: Vec<String>,
798    pub hyperedges: Vec<UnifiedHypergraphHyperedgeV0>,
799    pub summary_edges: Vec<HypergraphIFDSSummaryEdgeV0>,
800    pub gate_predicates: Vec<&'static str>,
801}
802
803#[derive(Debug, Clone, PartialEq, Eq, Serialize)]
804#[serde(rename_all = "camelCase")]
805pub struct OmenaQueryCrossFileSccEvidenceV0 {
806    pub schema_version: &'static str,
807    pub product: &'static str,
808    pub feature_gate: &'static str,
809    pub claim_level: &'static str,
810    pub theorem_claimed: bool,
811    pub connectivity_backend: &'static str,
812    pub polylog_bound_scope: &'static str,
813    pub scc_id: String,
814    pub node_count: usize,
815    pub directed_edge_count: usize,
816    pub cross_file: bool,
817    pub node_ids: Vec<String>,
818    pub style_paths: Vec<String>,
819    pub edge_kinds: Vec<&'static str>,
820    pub summary_edge_ids: Vec<String>,
821}
822
823#[derive(Debug, Clone, PartialEq, Eq, Serialize)]
824#[serde(rename_all = "camelCase")]
825pub struct OmenaQueryUnifiedCrossFileSccReportV0 {
826    pub schema_version: &'static str,
827    pub product: &'static str,
828    pub feature_gate: &'static str,
829    pub claim_level: &'static str,
830    pub theorem_claimed: bool,
831    pub connectivity_backend: &'static str,
832    pub polylog_bound_scope: &'static str,
833    pub node_count: usize,
834    pub directed_edge_count: usize,
835    pub cyclic_scc_count: usize,
836    pub sccs: Vec<OmenaQueryCrossFileSccEvidenceV0>,
837    pub gate_predicates: Vec<&'static str>,
838}
839
840#[derive(Debug, Clone, PartialEq, Eq)]
841pub struct HypergraphClosurePath<N> {
842    pub origin: N,
843    pub target: N,
844    pub depth: usize,
845    pub path_labels: Vec<String>,
846}
847
848#[derive(Debug, Clone, Copy, PartialEq, Eq)]
849pub enum HypergraphClosureMode {
850    CanonicalFirstTarget,
851    RawAllPaths,
852}
853
854pub trait OmenaUnifiedHypergraphConnectivityOracle {
855    fn reachable_node_ids(
856        &self,
857        start_node_id: &str,
858        hyperedges: &[UnifiedHypergraphHyperedgeV0],
859    ) -> Vec<String>;
860}
861
862#[derive(Debug, Clone, Copy, Default)]
863pub struct BatchHypergraphConnectivityOracle;
864
865impl OmenaUnifiedHypergraphConnectivityOracle for BatchHypergraphConnectivityOracle {
866    fn reachable_node_ids(
867        &self,
868        start_node_id: &str,
869        hyperedges: &[UnifiedHypergraphHyperedgeV0],
870    ) -> Vec<String> {
871        collect_reachable_node_ids(start_node_id, &build_adjacency(hyperedges))
872    }
873}
874
875/// The single shared reachability BFS loop for the cross-file engine: forward closure over a
876/// deterministic `BTreeMap` adjacency, returned sorted (BTreeSet order). Generic over the
877/// adjacency key type so each caller keeps its OWN adjacency builder (the distinct node spaces
878/// comment-2 point 4 demands) while sharing exactly one traversal loop. (The substrate diagnostics
879/// reachability is a separate borrowed-`&str` DFS over inline-filtered `resolved` edges and is
880/// intentionally NOT one of the two BFS impls this collapses.)
881pub fn collect_reachable_node_ids<K>(
882    start_node_id: &str,
883    adjacency: &BTreeMap<K, BTreeSet<String>>,
884) -> Vec<String>
885where
886    K: Ord + std::borrow::Borrow<str>,
887{
888    let mut seen = BTreeSet::new();
889    let mut pending = VecDeque::from([start_node_id.to_string()]);
890    while let Some(current) = pending.pop_front() {
891        for target in adjacency.get(current.as_str()).into_iter().flatten() {
892            if seen.insert(target.clone()) {
893                pending.push_back(target.clone());
894            }
895        }
896    }
897    seen.into_iter().collect()
898}
899
900pub fn collect_reachable_node_ids_bitset<K>(
901    start_node_id: &str,
902    adjacency: &BTreeMap<K, BTreeSet<String>>,
903) -> Vec<String>
904where
905    K: Ord + std::borrow::Borrow<str>,
906{
907    let dense_index = DenseNodeIndexV0::from_adjacency(start_node_id, adjacency);
908    let Some(start_index) = dense_index.index_of(start_node_id) else {
909        return Vec::new();
910    };
911    let dense_adjacency = dense_index.dense_adjacency(adjacency);
912    let mut seen = DenseBitsetV0::new(dense_index.len());
913    let mut pending = VecDeque::from([start_index]);
914    while let Some(current) = pending.pop_front() {
915        for target in dense_adjacency
916            .get(current)
917            .into_iter()
918            .flat_map(|targets| targets.iter().copied())
919        {
920            if seen.insert(target) {
921                pending.push_back(target);
922            }
923        }
924    }
925    dense_index.ids_for_bitset(&seen)
926}
927
928#[derive(Debug, Clone)]
929struct DenseNodeIndexV0 {
930    ids: Vec<String>,
931    positions: BTreeMap<String, usize>,
932}
933
934impl DenseNodeIndexV0 {
935    fn from_adjacency<K>(start_node_id: &str, adjacency: &BTreeMap<K, BTreeSet<String>>) -> Self
936    where
937        K: Ord + std::borrow::Borrow<str>,
938    {
939        let mut ids = BTreeSet::from([start_node_id.to_string()]);
940        for (source, targets) in adjacency {
941            ids.insert(source.borrow().to_string());
942            ids.extend(targets.iter().cloned());
943        }
944        let ids = ids.into_iter().collect::<Vec<_>>();
945        let positions = ids
946            .iter()
947            .enumerate()
948            .map(|(index, node_id)| (node_id.clone(), index))
949            .collect::<BTreeMap<_, _>>();
950        Self { ids, positions }
951    }
952
953    fn len(&self) -> usize {
954        self.ids.len()
955    }
956
957    fn index_of(&self, node_id: &str) -> Option<usize> {
958        self.positions.get(node_id).copied()
959    }
960
961    fn dense_adjacency<K>(&self, adjacency: &BTreeMap<K, BTreeSet<String>>) -> Vec<Vec<usize>>
962    where
963        K: Ord + std::borrow::Borrow<str>,
964    {
965        let mut dense_adjacency = vec![Vec::new(); self.ids.len()];
966        for (source, targets) in adjacency {
967            let Some(source_index) = self.index_of(source.borrow()) else {
968                continue;
969            };
970            dense_adjacency[source_index] = targets
971                .iter()
972                .filter_map(|target| self.index_of(target))
973                .collect();
974        }
975        dense_adjacency
976    }
977
978    fn ids_for_bitset(&self, bitset: &DenseBitsetV0) -> Vec<String> {
979        self.ids
980            .iter()
981            .enumerate()
982            .filter(|(index, _)| bitset.contains(*index))
983            .map(|(_, node_id)| node_id.clone())
984            .collect()
985    }
986}
987
988#[derive(Debug, Clone)]
989struct DenseBitsetV0 {
990    words: Vec<u64>,
991}
992
993impl DenseBitsetV0 {
994    fn new(len: usize) -> Self {
995        Self {
996            words: vec![0; len.div_ceil(64)],
997        }
998    }
999
1000    fn insert(&mut self, index: usize) -> bool {
1001        let word_index = index / 64;
1002        let mask = 1u64 << (index % 64);
1003        let word = &mut self.words[word_index];
1004        let was_empty = *word & mask == 0;
1005        *word |= mask;
1006        was_empty
1007    }
1008
1009    fn contains(&self, index: usize) -> bool {
1010        self.words
1011            .get(index / 64)
1012            .is_some_and(|word| word & (1u64 << (index % 64)) != 0)
1013    }
1014}
1015
1016pub fn tabulate_hypergraph_ifds_summary_edges(
1017    hyperedges: &[UnifiedHypergraphHyperedgeV0],
1018    projected_edges: Vec<HypergraphIFDSSummaryEdgeV0>,
1019) -> Vec<HypergraphIFDSSummaryEdgeV0> {
1020    let hyperedge_ids = hyperedges
1021        .iter()
1022        .map(|edge| edge.hyperedge_id.as_str())
1023        .collect::<BTreeSet<_>>();
1024    let mut edges = projected_edges
1025        .into_iter()
1026        .filter(|edge| hyperedge_ids.contains(edge.hyperedge_id.as_str()))
1027        .collect::<Vec<_>>();
1028    edges.sort_by(|left, right| {
1029        left.projection_edge_id
1030            .cmp(&right.projection_edge_id)
1031            .then(left.hyperedge_id.cmp(&right.hyperedge_id))
1032    });
1033    edges
1034}
1035
1036pub fn summarize_omena_query_unified_cross_file_scc_report(
1037    hypergraph: &OmenaQueryUnifiedCrossFileHypergraphV0,
1038) -> OmenaQueryUnifiedCrossFileSccReportV0 {
1039    let adjacency = build_directed_projection_adjacency(&hypergraph.summary_edges);
1040    let mut sccs = collect_directed_graph_sccs(&adjacency)
1041        .into_iter()
1042        .filter_map(|node_ids| summarize_cyclic_scc(&node_ids, &hypergraph.summary_edges))
1043        .collect::<Vec<_>>();
1044    sccs.sort_by(|left, right| {
1045        left.node_ids
1046            .cmp(&right.node_ids)
1047            .then(left.summary_edge_ids.cmp(&right.summary_edge_ids))
1048    });
1049    for (index, scc) in sccs.iter_mut().enumerate() {
1050        scc.scc_id = format!("exact-tarjan-scc:{}", index + 1);
1051    }
1052
1053    OmenaQueryUnifiedCrossFileSccReportV0 {
1054        schema_version: "0",
1055        product: "omena-query.unified-cross-file-scc-report",
1056        feature_gate: "cross-file-scc-v0",
1057        claim_level: "fixtureWitnessExactTarjanScc",
1058        theorem_claimed: false,
1059        connectivity_backend: "exactTarjanScc",
1060        polylog_bound_scope: "notClaimedExactTraversal",
1061        node_count: adjacency.len(),
1062        directed_edge_count: hypergraph
1063            .summary_edges
1064            .iter()
1065            .filter(|edge| summary_edge_has_supported_target(edge.status))
1066            .count(),
1067        cyclic_scc_count: sccs.len(),
1068        sccs,
1069        gate_predicates: vec![
1070            "exactTarjanSccBackend",
1071            "theorem_claimed=false",
1072            "polylog_bound_scope=notClaimedExactTraversal",
1073        ],
1074    }
1075}
1076
1077pub fn summarize_omena_query_unified_cross_file_hypergraph(
1078    summary: &OmenaQueryCrossFileSummaryV0,
1079) -> OmenaQueryUnifiedCrossFileHypergraphV0 {
1080    let mut builder = UnifiedCrossFileHypergraphBuilder::default();
1081    for edge in &summary.edges {
1082        builder.add_summary_edge(edge);
1083    }
1084    builder.finish()
1085}
1086
1087pub fn collect_hypergraph_transitive_closure_paths<N, F>(
1088    graph: &BTreeMap<N, BTreeSet<N>>,
1089    mut label: F,
1090) -> (Vec<HypergraphClosurePath<N>>, Vec<Vec<String>>)
1091where
1092    N: Clone + Ord,
1093    F: FnMut(&N) -> String,
1094{
1095    collect_hypergraph_transitive_closure_paths_with_mode(
1096        graph,
1097        &mut label,
1098        HypergraphClosureMode::CanonicalFirstTarget,
1099    )
1100}
1101
1102pub fn collect_hypergraph_transitive_closure_paths_with_mode<N, F>(
1103    graph: &BTreeMap<N, BTreeSet<N>>,
1104    label: &mut F,
1105    mode: HypergraphClosureMode,
1106) -> (Vec<HypergraphClosurePath<N>>, Vec<Vec<String>>)
1107where
1108    N: Clone + Ord,
1109    F: FnMut(&N) -> String,
1110{
1111    let mut closure_paths = Vec::new();
1112    let mut cycle_paths = Vec::new();
1113    let mut seen_cycles = BTreeSet::new();
1114    let first_target = mode == HypergraphClosureMode::CanonicalFirstTarget;
1115
1116    for start in graph.keys() {
1117        let mut visited = BTreeSet::new();
1118        let mut pending = VecDeque::from([(start.clone(), vec![start.clone()])]);
1119        while let Some((current, path)) = pending.pop_front() {
1120            for target in graph.get(&current).into_iter().flatten() {
1121                if let Some(cycle_start) = path.iter().position(|node| node == target) {
1122                    let mut cycle = path[cycle_start..].to_vec();
1123                    cycle.push(target.clone());
1124                    let mut labels = cycle.iter().map(&mut *label).collect::<Vec<_>>();
1125                    if first_target {
1126                        labels = canonical_hypergraph_cycle_labels(labels);
1127                    }
1128                    if !labels.is_empty() && seen_cycles.insert(labels.clone()) {
1129                        cycle_paths.push(labels);
1130                    }
1131                    continue;
1132                }
1133                if first_target && !visited.insert(target.clone()) {
1134                    continue;
1135                }
1136                let mut edge_path = path.clone();
1137                edge_path.push(target.clone());
1138                closure_paths.push(HypergraphClosurePath {
1139                    origin: start.clone(),
1140                    target: target.clone(),
1141                    depth: edge_path.len().saturating_sub(1),
1142                    path_labels: edge_path.iter().map(&mut *label).collect(),
1143                });
1144                pending.push_back((target.clone(), edge_path));
1145            }
1146        }
1147    }
1148    (closure_paths, cycle_paths)
1149}
1150
1151/// Generous per-SCC enumeration work budget; real module-graph SCCs are tiny (a cycle is a user
1152/// error to report), so the cap is a backstop that never fires in practice.
1153const DEFAULT_CYCLE_ENUMERATION_WORK_CAP: usize = 1 << 16;
1154
1155/// All elementary directed circuits of `adjacency`, found per strongly-connected component:
1156/// partition with `collect_directed_graph_sccs`, then enumerate the simple cycles CONFINED to each
1157/// non-trivial SCC (a back-edge to the start closes a circuit). Each circuit is canonicalized via
1158/// `canonical_hypergraph_cycle_labels` (lex-smallest rotation, emitted CLOSED so a consumer's
1159/// `windows(2)` successor lookup resolves), deduped and sorted. This is the cross-file CYCLE owner,
1160/// decoupled from the all-paths closure scan so the latter can be replaced without touching cycles.
1161pub fn collect_directed_graph_cycles(
1162    adjacency: &BTreeMap<String, BTreeSet<String>>,
1163) -> Vec<Vec<String>> {
1164    collect_directed_graph_cycles_with_work_cap(adjacency, DEFAULT_CYCLE_ENUMERATION_WORK_CAP)
1165}
1166
1167/// `collect_directed_graph_cycles` with an explicit per-SCC work cap (for tests). On cap-hit a
1168/// dense SCC degrades to its lex-smallest representative circuit — a witnessed shrink, NEVER a
1169/// silent drop.
1170pub fn collect_directed_graph_cycles_with_work_cap(
1171    adjacency: &BTreeMap<String, BTreeSet<String>>,
1172    per_scc_work_cap: usize,
1173) -> Vec<Vec<String>> {
1174    let mut circuits = BTreeSet::new();
1175    for scc in collect_directed_graph_sccs(adjacency) {
1176        let self_loop = scc.len() == 1
1177            && adjacency
1178                .get(scc[0].as_str())
1179                .is_some_and(|targets| targets.contains(&scc[0]));
1180        if scc.len() < 2 && !self_loop {
1181            continue;
1182        }
1183        let scc_nodes = scc.iter().map(String::as_str).collect::<BTreeSet<_>>();
1184        let mut found = BTreeSet::new();
1185        let mut work = 0usize;
1186        let mut capped = false;
1187        'starts: for start in &scc {
1188            // BFS over simple paths so the shortest circuits surface first (so the cap-fallback
1189            // representative is a real, short circuit).
1190            let mut pending = VecDeque::from([(start.clone(), vec![start.clone()])]);
1191            while let Some((current, path)) = pending.pop_front() {
1192                work += 1;
1193                if work > per_scc_work_cap {
1194                    capped = true;
1195                    break 'starts;
1196                }
1197                for target in adjacency.get(current.as_str()).into_iter().flatten() {
1198                    if !scc_nodes.contains(target.as_str()) {
1199                        continue;
1200                    }
1201                    if target == start {
1202                        let mut ring = path.clone();
1203                        ring.push(target.clone());
1204                        let canonical = canonical_hypergraph_cycle_labels(ring);
1205                        if !canonical.is_empty() {
1206                            found.insert(canonical);
1207                        }
1208                    } else if !path.iter().any(|node| node == target) {
1209                        let mut next = path.clone();
1210                        next.push(target.clone());
1211                        pending.push_back((target.clone(), next));
1212                    }
1213                }
1214            }
1215        }
1216        if capped {
1217            circuits.extend(found.into_iter().next());
1218        } else {
1219            circuits.extend(found);
1220        }
1221    }
1222    circuits.into_iter().collect()
1223}
1224
1225fn canonical_hypergraph_cycle_labels(mut labels: Vec<String>) -> Vec<String> {
1226    if labels.len() > 1 && labels.first() == labels.last() {
1227        labels.pop();
1228    }
1229    if labels.is_empty() {
1230        return labels;
1231    }
1232    let mut best = labels.clone();
1233    for offset in 1..labels.len() {
1234        let mut rotated = labels[offset..].to_vec();
1235        rotated.extend_from_slice(&labels[..offset]);
1236        best = best.min(rotated);
1237    }
1238    best.push(best[0].clone());
1239    best
1240}
1241
1242fn build_adjacency(
1243    hyperedges: &[UnifiedHypergraphHyperedgeV0],
1244) -> BTreeMap<&str, BTreeSet<String>> {
1245    let mut adjacency = BTreeMap::<&str, BTreeSet<String>>::new();
1246    for edge in hyperedges {
1247        for tail in &edge.tail_node_ids {
1248            adjacency
1249                .entry(tail.as_str())
1250                .or_default()
1251                .insert(edge.head_node_id.clone());
1252        }
1253    }
1254    adjacency
1255}
1256
1257#[derive(Default)]
1258struct UnifiedCrossFileHypergraphBuilder {
1259    node_ids: BTreeSet<String>,
1260    hyperedges: Vec<UnifiedHypergraphHyperedgeV0>,
1261    summary_edges: Vec<HypergraphIFDSSummaryEdgeV0>,
1262}
1263
1264impl UnifiedCrossFileHypergraphBuilder {
1265    fn add_summary_edge(&mut self, edge: &OmenaQueryCrossFileSummaryEdgeV0) {
1266        let edge_kind = unified_edge_kind_for_summary_edge(edge);
1267        let from_node_id = endpoint_node_id(edge, false);
1268        let to_node_id = endpoint_node_id(edge, true);
1269        let tail_node_ids = if edge_kind.is_order_significant() && !edge.target_names.is_empty() {
1270            edge.target_names
1271                .iter()
1272                .map(|target_name| {
1273                    node_id(
1274                        "styleSymbol",
1275                        edge.target_path
1276                            .as_deref()
1277                            .unwrap_or(edge.from_path.as_str()),
1278                        Some(target_name),
1279                    )
1280                })
1281                .collect::<Vec<_>>()
1282        } else {
1283            vec![from_node_id.clone()]
1284        };
1285        self.node_ids.insert(from_node_id.clone());
1286        self.node_ids.insert(to_node_id.clone());
1287        self.node_ids.extend(tail_node_ids.iter().cloned());
1288
1289        let hyperedge_id = format!(
1290            "hyperedge:{}|{}|{}",
1291            edge_kind.as_wire_label(),
1292            edge.edge_id,
1293            tail_node_ids.join(">")
1294        );
1295        self.hyperedges.push(UnifiedHypergraphHyperedgeV0 {
1296            schema_version: "0",
1297            product: "omena-query.unified-hypergraph-hyperedge",
1298            layer_marker: "hypergraph-ifds",
1299            feature_gate: "hypergraph-ifds",
1300            hyperedge_id: hyperedge_id.clone(),
1301            edge_kind,
1302            source_summary_edge_id: edge.edge_id.clone(),
1303            source_edge_kind: edge.edge_kind,
1304            source_status: edge.status,
1305            tail_node_ids,
1306            head_node_id: to_node_id.clone(),
1307            order_significant_tail: edge_kind.is_order_significant(),
1308        });
1309        self.summary_edges.push(HypergraphIFDSSummaryEdgeV0 {
1310            schema_version: "0",
1311            product: "omena-query.hypergraph-ifds-summary-edge",
1312            layer_marker: "hypergraph-ifds",
1313            feature_gate: "hypergraph-ifds",
1314            summary_edge_id: format!("ifds-summary:{}", edge.edge_id),
1315            projection_edge_id: edge.edge_id.clone(),
1316            hyperedge_id,
1317            from_node_id,
1318            to_node_id,
1319            edge_kind,
1320            status: edge.status,
1321            provenance: edge.provenance.clone(),
1322            linear_provenance: edge.linear_provenance.clone(),
1323        });
1324    }
1325
1326    fn finish(mut self) -> OmenaQueryUnifiedCrossFileHypergraphV0 {
1327        self.hyperedges
1328            .sort_by_key(|edge| edge.hyperedge_id.clone());
1329        let summary_edges =
1330            tabulate_hypergraph_ifds_summary_edges(&self.hyperedges, self.summary_edges);
1331        let projection_edge_ids = summary_edges
1332            .iter()
1333            .map(|edge| edge.projection_edge_id.clone())
1334            .collect::<Vec<_>>();
1335
1336        OmenaQueryUnifiedCrossFileHypergraphV0 {
1337            schema_version: "0",
1338            product: "omena-query.unified-cross-file-hypergraph",
1339            status: "hypergraphIfdsProjection",
1340            layer_marker: "hypergraph-ifds",
1341            feature_gate: "hypergraph-ifds",
1342            node_count: self.node_ids.len(),
1343            hyperedge_count: self.hyperedges.len(),
1344            summary_edge_count: summary_edges.len(),
1345            projection_edge_ids,
1346            hyperedges: self.hyperedges,
1347            summary_edges,
1348            gate_predicates: vec![
1349                "P1.typeIntroduction",
1350                "P2.byteEqualAdjacencyProjection",
1351                "P3.sccUnification",
1352                "P4.summaryEdgeSetEquality",
1353                "P5.projectionHelper",
1354                "P6.closureBodySwitchOver",
1355                "P7.v0Publication",
1356                "batchConnectivityOracle",
1357                "streamingOracleWireCompatible",
1358                "composesTailOrderingUsesVec",
1359            ],
1360        }
1361    }
1362}
1363
1364fn endpoint_node_id(edge: &OmenaQueryCrossFileSummaryEdgeV0, target: bool) -> String {
1365    let (kind, path, symbol) = if target {
1366        (
1367            node_kind_for_summary_kind(edge.target_kind.unwrap_or(edge.from_kind), true),
1368            edge.target_path
1369                .as_deref()
1370                .unwrap_or(edge.from_path.as_str()),
1371            edge.remote_name
1372                .as_deref()
1373                .or_else(|| edge.target_names.first().map(String::as_str)),
1374        )
1375    } else {
1376        (
1377            node_kind_for_summary_kind(edge.from_kind, false),
1378            edge.from_path.as_str(),
1379            edge.owner_selector_name
1380                .as_deref()
1381                .or(edge.local_name.as_deref()),
1382        )
1383    };
1384    node_id(kind, path, symbol)
1385}
1386
1387fn node_id(kind: &'static str, path: &str, symbol: Option<&str>) -> String {
1388    format!("{}|{}|{}", kind, path, symbol.unwrap_or("-"))
1389}
1390
1391fn node_kind_for_summary_kind(kind: &str, target: bool) -> &'static str {
1392    match (kind, target) {
1393        ("style", false) => "styleModule",
1394        ("style", true) => "styleSymbol",
1395        ("source", false) => "sourceModule",
1396        ("source", true) => "sourceSymbol",
1397        _ => "foreignSymbol",
1398    }
1399}
1400
1401fn unified_edge_kind_for_summary_edge(
1402    edge: &OmenaQueryCrossFileSummaryEdgeV0,
1403) -> UnifiedHypergraphEdgeKindV0 {
1404    match edge.edge_kind {
1405        "composesLocal" => UnifiedHypergraphEdgeKindV0::ComposesLocal,
1406        "composesGlobal" => UnifiedHypergraphEdgeKindV0::ComposesGlobal,
1407        "cssModulesComposesImport" | "cssModulesComposesClosure" | "composesExternal" => {
1408            UnifiedHypergraphEdgeKindV0::ComposesExternal
1409        }
1410        "sassUse" => UnifiedHypergraphEdgeKindV0::SassUse,
1411        "sassForward" => UnifiedHypergraphEdgeKindV0::SassForward,
1412        "sassImport" => UnifiedHypergraphEdgeKindV0::SassImport,
1413        "lessImport" => UnifiedHypergraphEdgeKindV0::LessImport,
1414        "lessModuleGraphClosure" => UnifiedHypergraphEdgeKindV0::LessModuleGraphClosure,
1415        "cssModulesValueImport" | "cssModulesValueClosure" | "value" => {
1416            UnifiedHypergraphEdgeKindV0::Value
1417        }
1418        "cssModulesIcssImport" | "cssModulesIcssClosure" | "icss" => {
1419            UnifiedHypergraphEdgeKindV0::Icss
1420        }
1421        _ => UnifiedHypergraphEdgeKindV0::ForeignReference,
1422    }
1423}
1424
1425fn build_directed_projection_adjacency(
1426    summary_edges: &[HypergraphIFDSSummaryEdgeV0],
1427) -> BTreeMap<String, BTreeSet<String>> {
1428    let mut adjacency = BTreeMap::<String, BTreeSet<String>>::new();
1429    for edge in summary_edges {
1430        if !summary_edge_has_supported_target(edge.status) {
1431            continue;
1432        }
1433        let from_node_id = canonical_scc_node_id(edge.from_node_id.as_str());
1434        let to_node_id = canonical_scc_node_id(edge.to_node_id.as_str());
1435        adjacency.entry(from_node_id.clone()).or_default();
1436        adjacency.entry(to_node_id.clone()).or_default();
1437        adjacency
1438            .entry(from_node_id)
1439            .or_default()
1440            .insert(to_node_id);
1441    }
1442    adjacency
1443}
1444
1445/// The single shared strongly-connected-components primitive for the cross-file engine: exact
1446/// Tarjan over a deterministic `BTreeMap` adjacency, each component sorted, components in Tarjan
1447/// reverse-topological discovery order. Consumed by both this crate's unified SCC report and
1448/// `omena-streaming-ifds` (which previously carried a byte-identical duplicate).
1449pub fn collect_directed_graph_sccs(
1450    adjacency: &BTreeMap<String, BTreeSet<String>>,
1451) -> Vec<Vec<String>> {
1452    let mut state = TarjanState::default();
1453    for node_id in adjacency.keys() {
1454        if !state.indices.contains_key(node_id) {
1455            state.visit(node_id, adjacency);
1456        }
1457    }
1458    state.components
1459}
1460
1461#[derive(Default)]
1462struct TarjanState {
1463    next_index: usize,
1464    stack: Vec<String>,
1465    on_stack: BTreeSet<String>,
1466    indices: BTreeMap<String, usize>,
1467    lowlinks: BTreeMap<String, usize>,
1468    components: Vec<Vec<String>>,
1469}
1470
1471impl TarjanState {
1472    fn visit(&mut self, node_id: &str, adjacency: &BTreeMap<String, BTreeSet<String>>) {
1473        let index = self.next_index;
1474        self.next_index += 1;
1475        self.indices.insert(node_id.to_string(), index);
1476        self.lowlinks.insert(node_id.to_string(), index);
1477        self.stack.push(node_id.to_string());
1478        self.on_stack.insert(node_id.to_string());
1479
1480        if let Some(targets) = adjacency.get(node_id) {
1481            for target in targets {
1482                if !self.indices.contains_key(target.as_str()) {
1483                    self.visit(target, adjacency);
1484                    let target_lowlink = self.lowlinks[target.as_str()];
1485                    let current_lowlink = self.lowlinks[node_id];
1486                    self.lowlinks
1487                        .insert(node_id.to_string(), current_lowlink.min(target_lowlink));
1488                } else if self.on_stack.contains(target.as_str()) {
1489                    let target_index = self.indices[target.as_str()];
1490                    let current_lowlink = self.lowlinks[node_id];
1491                    self.lowlinks
1492                        .insert(node_id.to_string(), current_lowlink.min(target_index));
1493                }
1494            }
1495        }
1496
1497        if self.lowlinks[node_id] == self.indices[node_id] {
1498            let mut component = Vec::new();
1499            while let Some(stack_node) = self.stack.pop() {
1500                self.on_stack.remove(stack_node.as_str());
1501                let done = stack_node == node_id;
1502                component.push(stack_node);
1503                if done {
1504                    break;
1505                }
1506            }
1507            component.sort();
1508            self.components.push(component);
1509        }
1510    }
1511}
1512
1513fn summarize_cyclic_scc(
1514    node_ids: &[String],
1515    summary_edges: &[HypergraphIFDSSummaryEdgeV0],
1516) -> Option<OmenaQueryCrossFileSccEvidenceV0> {
1517    let node_set = node_ids.iter().map(String::as_str).collect::<BTreeSet<_>>();
1518    let internal_edges = summary_edges
1519        .iter()
1520        .filter(|edge| summary_edge_has_supported_target(edge.status))
1521        .filter(|edge| {
1522            let from_node_id = canonical_scc_node_id(edge.from_node_id.as_str());
1523            let to_node_id = canonical_scc_node_id(edge.to_node_id.as_str());
1524            node_set.contains(from_node_id.as_str()) && node_set.contains(to_node_id.as_str())
1525        })
1526        .collect::<Vec<_>>();
1527    let has_self_loop = internal_edges.iter().any(|edge| {
1528        canonical_scc_node_id(edge.from_node_id.as_str())
1529            == canonical_scc_node_id(edge.to_node_id.as_str())
1530    });
1531    if node_ids.len() < 2 && !has_self_loop {
1532        return None;
1533    }
1534
1535    let style_paths = node_ids
1536        .iter()
1537        .filter_map(|node_id| style_path_from_node_id(node_id))
1538        .collect::<BTreeSet<_>>()
1539        .into_iter()
1540        .collect::<Vec<_>>();
1541    let edge_kinds = internal_edges
1542        .iter()
1543        .map(|edge| edge.edge_kind.as_wire_label())
1544        .collect::<BTreeSet<_>>()
1545        .into_iter()
1546        .collect::<Vec<_>>();
1547    let summary_edge_ids = internal_edges
1548        .iter()
1549        .map(|edge| edge.projection_edge_id.clone())
1550        .collect::<BTreeSet<_>>()
1551        .into_iter()
1552        .collect::<Vec<_>>();
1553
1554    Some(OmenaQueryCrossFileSccEvidenceV0 {
1555        schema_version: "0",
1556        product: "omena-query.cross-file-scc-evidence",
1557        feature_gate: "cross-file-scc-v0",
1558        claim_level: "fixtureWitnessExactTarjanScc",
1559        theorem_claimed: false,
1560        connectivity_backend: "exactTarjanScc",
1561        polylog_bound_scope: "notClaimedExactTraversal",
1562        scc_id: String::new(),
1563        node_count: node_ids.len(),
1564        directed_edge_count: internal_edges.len(),
1565        cross_file: style_paths.len() > 1,
1566        node_ids: node_ids.to_vec(),
1567        style_paths,
1568        edge_kinds,
1569        summary_edge_ids,
1570    })
1571}
1572
1573fn style_path_from_node_id(node_id: &str) -> Option<String> {
1574    let mut parts = node_id.splitn(3, '|');
1575    let _kind = parts.next()?;
1576    let path = parts.next()?;
1577    Some(path.to_string())
1578}
1579
1580fn canonical_scc_node_id(node_id: &str) -> String {
1581    let mut parts = node_id.splitn(3, '|');
1582    let Some(kind) = parts.next() else {
1583        return node_id.to_string();
1584    };
1585    let Some(path) = parts.next() else {
1586        return node_id.to_string();
1587    };
1588    let Some(symbol) = parts.next() else {
1589        return node_id.to_string();
1590    };
1591    if kind == "styleModule" && symbol != "-" {
1592        return format!("styleSymbol|{path}|{symbol}");
1593    }
1594    node_id.to_string()
1595}
1596
1597fn summary_edge_has_supported_target(status: &str) -> bool {
1598    matches!(
1599        status,
1600        "resolved" | "reachable" | "localResolved" | "importResolved" | "external"
1601    )
1602}
1603
1604fn stable_omena_query_cross_file_summary_hash(
1605    edges: &[OmenaQueryCrossFileSummaryEdgeV0],
1606) -> String {
1607    let mut hash = 0xcbf29ce484222325u64;
1608    stable_omena_query_hash_piece(&mut hash, "omena-query.cross-file-summary");
1609    stable_omena_query_hash_piece(&mut hash, "0");
1610    for edge in edges {
1611        stable_omena_query_hash_piece(&mut hash, edge.edge_id.as_str());
1612        stable_omena_query_hash_piece(&mut hash, edge.status);
1613        stable_omena_query_hash_piece(&mut hash, edge.linear_provenance.semiring_identifier());
1614        let term_count = edge.linear_provenance.term_count.to_string();
1615        stable_omena_query_hash_piece(&mut hash, term_count.as_str());
1616        for term in &edge.linear_provenance.terms {
1617            let coefficient = term.coefficient.to_string();
1618            stable_omena_query_hash_piece(&mut hash, coefficient.as_str());
1619            stable_omena_query_hash_piece(&mut hash, term.label);
1620        }
1621    }
1622    format!("{hash:016x}")
1623}
1624
1625fn stable_omena_query_hash_piece(hash: &mut u64, piece: &str) {
1626    for byte in piece.as_bytes() {
1627        *hash ^= u64::from(*byte);
1628        *hash = hash.wrapping_mul(0x100000001b3);
1629    }
1630    *hash ^= 0xff;
1631    *hash = hash.wrapping_mul(0x100000001b3);
1632}
1633
1634#[cfg(test)]
1635mod tests {
1636    use super::*;
1637
1638    #[test]
1639    fn bitset_reachability_matches_btreeset_closure_order_and_cycles() {
1640        let adjacency = BTreeMap::from([
1641            (
1642                "module:root".to_string(),
1643                BTreeSet::from(["symbol:base".to_string(), "symbol:theme".to_string()]),
1644            ),
1645            (
1646                "symbol:base".to_string(),
1647                BTreeSet::from(["symbol:terminal".to_string()]),
1648            ),
1649            (
1650                "symbol:theme".to_string(),
1651                BTreeSet::from(["module:root".to_string(), "symbol:terminal".to_string()]),
1652            ),
1653        ]);
1654
1655        let btreeset = collect_reachable_node_ids("module:root", &adjacency);
1656        let bitset = collect_reachable_node_ids_bitset("module:root", &adjacency);
1657
1658        assert_eq!(
1659            bitset,
1660            vec![
1661                "module:root".to_string(),
1662                "symbol:base".to_string(),
1663                "symbol:terminal".to_string(),
1664                "symbol:theme".to_string(),
1665            ]
1666        );
1667        assert_eq!(bitset, btreeset);
1668    }
1669
1670    #[test]
1671    fn reverse_dependency_delta_matches_from_scratch_index_across_edge_edits() {
1672        let sequences = reverse_dependency_edit_sequences();
1673        assert!(sequences.len() >= 12);
1674        assert!(
1675            sequences
1676                .iter()
1677                .any(|sequence| sequence_has_add_and_remove_for_same_origin(sequence)),
1678            "fixture corpus must include add/remove edits for an existing origin"
1679        );
1680
1681        for sequence in sequences {
1682            let mut incremental = reverse_dependency_index_from_edges_v0(&[]);
1683            let mut patched_total = 0;
1684            for next_edges in &sequence {
1685                patched_total += apply_reverse_dependency_delta_v0(&mut incremental, next_edges);
1686            }
1687            let final_edges = sequence.last().map(Vec::as_slice).unwrap_or(&[]);
1688            let from_scratch = reverse_dependency_index_from_edges_v0(final_edges);
1689
1690            assert_eq!(incremental, from_scratch);
1691            assert_eq!(
1692                reverse_dependency_index_fingerprint(&incremental),
1693                reverse_dependency_index_fingerprint(&from_scratch)
1694            );
1695            assert!(patched_total > 0);
1696            assert_eq!(
1697                apply_reverse_dependency_delta_v0(&mut incremental, final_edges),
1698                0,
1699                "unchanged edge groups must not be patched"
1700            );
1701        }
1702    }
1703
1704    #[test]
1705    fn reverse_dependency_closure_keeps_transitive_source_dependents() {
1706        let edges = vec![
1707            fixture_edge(
1708                "style",
1709                "/workspace/src/Mid.module.scss",
1710                "style",
1711                "/workspace/src/Base.module.scss",
1712            ),
1713            fixture_edge(
1714                "source",
1715                "/workspace/src/App.tsx",
1716                "style",
1717                "/workspace/src/Mid.module.scss",
1718            ),
1719            fixture_edge(
1720                "source",
1721                "/workspace/src/Other.tsx",
1722                "style",
1723                "/workspace/src/Other.module.scss",
1724            ),
1725        ];
1726        let index = reverse_dependency_index_from_edges_v0(edges.as_slice());
1727        let seeds = BTreeSet::from(["/workspace/src/Base.module.scss".to_string()]);
1728        let closure = reverse_dependency_closure_v0(&index, &seeds);
1729
1730        assert!(closure.contains("/workspace/src/Mid.module.scss"));
1731        assert!(closure.contains("/workspace/src/App.tsx"));
1732        assert!(!closure.contains("/workspace/src/Other.tsx"));
1733    }
1734
1735    #[test]
1736    fn typed_vocabulary_keeps_raw_catalog_and_lossy_fold_visible() {
1737        assert_eq!(UNIFIED_HYPERGRAPH_EDGE_KIND_VARIANTS_V0.len(), 11);
1738        assert_eq!(CROSS_FILE_SUMMARY_RAW_EDGE_KIND_VARIANTS_V0.len(), 22);
1739        assert_eq!(CROSS_FILE_SUMMARY_RAW_EDGE_KIND_LABELS_V0.len(), 22);
1740        assert_eq!(
1741            CROSS_FILE_SUMMARY_RAW_EDGE_KIND_VARIANTS_V0
1742                .iter()
1743                .map(|kind| kind.as_wire_label())
1744                .collect::<Vec<_>>(),
1745            CROSS_FILE_SUMMARY_RAW_EDGE_KIND_LABELS_V0
1746        );
1747        assert!(
1748            CROSS_FILE_SUMMARY_RAW_EDGE_KIND_LABELS_V0.contains(&"sourceSelectorPrefixReference")
1749        );
1750
1751        let prefix_kind =
1752            parse_cross_file_summary_raw_edge_kind_v0("sourceSelectorPrefixReference");
1753        assert_eq!(
1754            prefix_kind.map(OmenaCrossFileSummaryRawEdgeKindV0::folded_edge_kind),
1755            Some(UnifiedHypergraphEdgeKindV0::ForeignReference)
1756        );
1757        assert_eq!(
1758            prefix_kind.map(OmenaCrossFileSummaryRawEdgeKindV0::folded_by_lossy_catch_all),
1759            Some(true)
1760        );
1761        assert!(parse_cross_file_summary_raw_edge_kind_v0("strayEdgeKind").is_none());
1762        assert!(parse_cross_file_summary_node_role_v0("runtime").is_none());
1763    }
1764
1765    #[test]
1766    fn raw_edge_catalog_has_total_order_relevance() {
1767        let classified = CROSS_FILE_SUMMARY_RAW_EDGE_KIND_VARIANTS_V0
1768            .iter()
1769            .map(|kind| (kind.as_wire_label(), kind.order_relevance().as_wire_label()))
1770            .collect::<Vec<_>>();
1771
1772        assert_eq!(
1773            classified.len(),
1774            CROSS_FILE_SUMMARY_RAW_EDGE_KIND_LABELS_V0.len()
1775        );
1776        assert!(classified.iter().all(|(kind, relevance)| {
1777            !kind.is_empty() && matches!(*relevance, "orderBearing" | "orderNeutral")
1778        }));
1779        assert_eq!(
1780            OmenaCrossFileSummaryRawEdgeKindV0::CssModulesImport.order_relevance(),
1781            EdgeOrderRelevanceV0::OrderBearing
1782        );
1783        assert_eq!(
1784            OmenaCrossFileSummaryRawEdgeKindV0::SourceSelectorReference.order_relevance(),
1785            EdgeOrderRelevanceV0::OrderNeutral
1786        );
1787    }
1788
1789    #[test]
1790    fn summary_view_recomputes_raw_counts_from_edges() {
1791        let edges = vec![
1792            fixture_edge_with_kind(
1793                "cssModulesComposesImport",
1794                "style",
1795                "/workspace/src/Button.module.scss",
1796                "style",
1797                "/workspace/src/Base.module.scss",
1798            ),
1799            fixture_edge_with_kind(
1800                "sourceSelectorPrefixReference",
1801                "source",
1802                "/workspace/src/Button.tsx",
1803                "style",
1804                "/workspace/src/Button.module.scss",
1805            ),
1806        ];
1807        let summary = summary_from_edges(edges);
1808        let view = summarize_cross_file_summary_view_v0(&summary);
1809        assert!(view.summary_view_ready);
1810        assert_eq!(view.recomputed_edge_kind_counts, summary.edge_kind_counts);
1811
1812        let mut perturbed = summary.clone();
1813        perturbed.edge_kind_counts = vec![OmenaQueryCrossFileSummaryEdgeKindCountV0 {
1814            edge_kind: "cssModulesComposesImport",
1815            count: 99,
1816        }];
1817        let perturbed_view = summarize_cross_file_summary_view_v0(&perturbed);
1818        assert!(!perturbed_view.edge_kind_counts_match_existing_field);
1819        assert_eq!(
1820            perturbed_view.recomputed_edge_kind_counts,
1821            summary.edge_kind_counts
1822        );
1823
1824        let invalid = summary_from_edges(vec![fixture_edge_with_kind(
1825            "strayEdgeKind",
1826            "style",
1827            "/workspace/src/Button.module.scss",
1828            "style",
1829            "/workspace/src/Base.module.scss",
1830        )]);
1831        let invalid_view = summarize_cross_file_summary_view_v0(&invalid);
1832        assert!(!invalid_view.all_raw_edge_kinds_in_catalog);
1833        assert_eq!(invalid_view.invalid_raw_edge_kinds, vec!["strayEdgeKind"]);
1834    }
1835
1836    #[test]
1837    fn graph_delta_records_typed_added_and_removed_edges() {
1838        let stable_edge = fixture_edge_with_kind(
1839            "sourceSelectorReference",
1840            "source",
1841            "/workspace/src/App.tsx",
1842            "style",
1843            "/workspace/src/App.module.scss",
1844        );
1845        let removed_edge = fixture_edge_with_kind(
1846            "cssModulesValueImport",
1847            "style",
1848            "/workspace/src/Tokens.module.scss",
1849            "style",
1850            "/workspace/src/LegacyTokens.module.scss",
1851        );
1852        let added_edge = fixture_edge_with_kind(
1853            "cssModulesComposesImport",
1854            "style",
1855            "/workspace/src/Button.module.scss",
1856            "style",
1857            "/workspace/src/Base.module.scss",
1858        );
1859        let before = summary_from_edges(vec![stable_edge.clone(), removed_edge.clone()]);
1860        let after = summary_from_edges(vec![stable_edge, added_edge.clone()]);
1861        let delta = summarize_cross_file_graph_delta_v0(&before, &after);
1862
1863        assert!(delta.all_delta_edges_typed);
1864        assert_eq!(delta.added_edges.len(), 1);
1865        assert_eq!(delta.removed_edges.len(), 1);
1866        assert_eq!(delta.added_edges[0].edge_id, added_edge.edge_id);
1867        assert_eq!(
1868            delta.added_edges[0].raw_edge_kind,
1869            "cssModulesComposesImport"
1870        );
1871        assert_eq!(
1872            delta.added_edges[0].folded_edge_kind,
1873            UnifiedHypergraphEdgeKindV0::ComposesExternal
1874        );
1875        assert_eq!(delta.removed_edges[0].edge_id, removed_edge.edge_id);
1876    }
1877
1878    fn reverse_dependency_edit_sequences() -> Vec<Vec<Vec<OmenaQueryCrossFileSummaryEdgeV0>>> {
1879        let empty = Vec::new();
1880        let source_a = fixture_edge(
1881            "source",
1882            "/workspace/src/App.tsx",
1883            "style",
1884            "/workspace/src/A.module.scss",
1885        );
1886        let source_b = fixture_edge(
1887            "source",
1888            "/workspace/src/App.tsx",
1889            "style",
1890            "/workspace/src/B.module.scss",
1891        );
1892        let other_b = fixture_edge(
1893            "source",
1894            "/workspace/src/Other.tsx",
1895            "style",
1896            "/workspace/src/B.module.scss",
1897        );
1898        let mid_a = fixture_edge(
1899            "style",
1900            "/workspace/src/Mid.module.scss",
1901            "style",
1902            "/workspace/src/A.module.scss",
1903        );
1904        let source_mid = fixture_edge(
1905            "source",
1906            "/workspace/src/App.tsx",
1907            "style",
1908            "/workspace/src/Mid.module.scss",
1909        );
1910        let leaf_mid = fixture_edge(
1911            "style",
1912            "/workspace/src/Leaf.module.scss",
1913            "style",
1914            "/workspace/src/Mid.module.scss",
1915        );
1916        let source_leaf = fixture_edge(
1917            "source",
1918            "/workspace/src/Deep.tsx",
1919            "style",
1920            "/workspace/src/Leaf.module.scss",
1921        );
1922        let value_a = fixture_edge(
1923            "style",
1924            "/workspace/src/Value.module.scss",
1925            "style",
1926            "/workspace/src/A.module.scss",
1927        );
1928
1929        vec![
1930            vec![
1931                empty.clone(),
1932                vec![source_a.clone()],
1933                vec![source_a.clone(), other_b.clone()],
1934            ],
1935            vec![
1936                vec![source_a.clone()],
1937                vec![source_b.clone()],
1938                vec![source_b.clone(), other_b.clone()],
1939            ],
1940            vec![
1941                vec![source_a.clone(), other_b.clone()],
1942                vec![other_b.clone()],
1943                vec![source_a.clone(), other_b.clone()],
1944            ],
1945            vec![
1946                vec![mid_a.clone()],
1947                vec![mid_a.clone(), source_mid.clone()],
1948                vec![source_mid.clone()],
1949            ],
1950            vec![
1951                vec![mid_a.clone(), source_mid.clone()],
1952                vec![mid_a.clone(), source_mid.clone(), leaf_mid.clone()],
1953                vec![leaf_mid.clone(), source_leaf.clone()],
1954            ],
1955            vec![
1956                vec![source_leaf.clone()],
1957                vec![source_leaf.clone(), value_a.clone()],
1958                vec![value_a.clone()],
1959            ],
1960            vec![
1961                empty.clone(),
1962                vec![source_b.clone(), other_b.clone()],
1963                vec![source_a.clone(), other_b.clone()],
1964            ],
1965            vec![vec![value_a.clone()], empty.clone(), vec![value_a.clone()]],
1966            vec![
1967                vec![leaf_mid.clone()],
1968                vec![leaf_mid.clone(), source_leaf.clone()],
1969                vec![source_leaf.clone()],
1970            ],
1971            vec![
1972                vec![source_mid.clone(), source_leaf.clone()],
1973                vec![source_mid.clone()],
1974                vec![source_mid.clone(), source_leaf.clone()],
1975            ],
1976            vec![
1977                vec![mid_a.clone(), value_a.clone()],
1978                vec![value_a.clone()],
1979                vec![mid_a.clone(), value_a.clone()],
1980            ],
1981            vec![
1982                vec![source_a.clone()],
1983                vec![source_a.clone(), mid_a.clone()],
1984                vec![mid_a, source_mid],
1985            ],
1986        ]
1987    }
1988
1989    fn sequence_has_add_and_remove_for_same_origin(
1990        sequence: &[Vec<OmenaQueryCrossFileSummaryEdgeV0>],
1991    ) -> bool {
1992        sequence.windows(3).any(|window| {
1993            let first = reverse_dependency_groups_by_from_path(window[0].as_slice());
1994            let second = reverse_dependency_groups_by_from_path(window[1].as_slice());
1995            let third = reverse_dependency_groups_by_from_path(window[2].as_slice());
1996            first.keys().any(|from_path| {
1997                let first_targets = first.get(from_path).cloned().unwrap_or_default();
1998                let second_targets = second.get(from_path).cloned().unwrap_or_default();
1999                let third_targets = third.get(from_path).cloned().unwrap_or_default();
2000                first_targets != second_targets
2001                    && first_targets == third_targets
2002                    && !first_targets.is_empty()
2003            })
2004        })
2005    }
2006
2007    fn fixture_edge(
2008        from_kind: &'static str,
2009        from_path: &str,
2010        target_kind: &'static str,
2011        target_path: &str,
2012    ) -> OmenaQueryCrossFileSummaryEdgeV0 {
2013        fixture_edge_with_kind(
2014            "fixtureDependency",
2015            from_kind,
2016            from_path,
2017            target_kind,
2018            target_path,
2019        )
2020    }
2021
2022    fn fixture_edge_with_kind(
2023        edge_kind: &'static str,
2024        from_kind: &'static str,
2025        from_path: &str,
2026        target_kind: &'static str,
2027        target_path: &str,
2028    ) -> OmenaQueryCrossFileSummaryEdgeV0 {
2029        OmenaQueryCrossFileSummaryEdgeV0 {
2030            edge_id: format!("{from_kind}:{from_path}->{target_kind}:{target_path}"),
2031            edge_kind,
2032            from_kind,
2033            from_path: from_path.to_string(),
2034            target_kind: Some(target_kind),
2035            target_path: Some(target_path.to_string()),
2036            source: None,
2037            owner_selector_name: None,
2038            local_name: None,
2039            remote_name: None,
2040            target_names: Vec::new(),
2041            status: "resolved",
2042            provenance: vec!["omena-query.cross-file-summary.fixture"],
2043            linear_provenance: OmenaCrossFileLinearProvenanceV0::from_static_labels(&[
2044                "omena-query.cross-file-summary.fixture",
2045            ]),
2046        }
2047    }
2048
2049    fn summary_from_edges(
2050        edges: Vec<OmenaQueryCrossFileSummaryEdgeV0>,
2051    ) -> OmenaQueryCrossFileSummaryV0 {
2052        OmenaQueryCrossFileSummaryV0 {
2053            schema_version: "0",
2054            product: "omena-query.cross-file-summary",
2055            status: "fixtureSummary",
2056            summary_scope: "workspaceStyleAndSource",
2057            style_count: 1,
2058            summary_edge_count: edges.len(),
2059            edge_kind_counts: recompute_cross_file_summary_raw_edge_kind_counts_v0(
2060                edges.as_slice(),
2061            ),
2062            summary_hash: stable_omena_query_cross_file_summary_hash(edges.as_slice()),
2063            edges,
2064            capabilities: OmenaQueryCrossFileSummaryCapabilitiesV0 {
2065                css_modules_composes_edges_ready: true,
2066                css_modules_value_edges_ready: true,
2067                css_modules_icss_edges_ready: true,
2068                sass_module_edges_ready: true,
2069                style_design_token_reference_edges_ready: true,
2070                source_selector_reference_edges_ready: true,
2071                stable_summary_hash_ready: true,
2072                linear_provenance_ready: true,
2073                linear_provenance_round_trip_ready: true,
2074                linear_provenance_semiring_laws_hold: true,
2075            },
2076            next_priorities: Vec::new(),
2077        }
2078    }
2079
2080    fn reverse_dependency_index_fingerprint(index: &ReverseDependencyIndexV0) -> String {
2081        let mut parts = Vec::new();
2082        for (target, dependents) in &index.rev {
2083            parts.push(format!(
2084                "rev:{target}={}",
2085                dependents.iter().cloned().collect::<Vec<_>>().join(",")
2086            ));
2087        }
2088        for (from_path, targets) in &index.edges_by_from {
2089            parts.push(format!(
2090                "from:{from_path}={}",
2091                targets.iter().cloned().collect::<Vec<_>>().join(",")
2092            ));
2093        }
2094        parts.join("|")
2095    }
2096}