1use 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
632pub 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 HypergraphMonotoneFactPropagationSummaryEdgeV0 {
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#[deprecated(
790 since = "0.4.0",
791 note = "use HypergraphMonotoneFactPropagationSummaryEdgeV0; removal is not before 1.0 and requires downstream migration plus zero audited non-compatibility uses"
792)]
793#[derive(Debug, Clone, PartialEq, Eq, Serialize)]
794#[serde(rename_all = "camelCase")]
795pub struct HypergraphIFDSSummaryEdgeV0 {
796 pub schema_version: &'static str,
797 pub product: &'static str,
798 pub layer_marker: &'static str,
799 pub feature_gate: &'static str,
800 pub summary_edge_id: String,
801 pub projection_edge_id: String,
802 pub hyperedge_id: String,
803 pub from_node_id: String,
804 pub to_node_id: String,
805 pub edge_kind: UnifiedHypergraphEdgeKindV0,
806 pub status: &'static str,
807 pub provenance: Vec<&'static str>,
808 pub linear_provenance: OmenaCrossFileLinearProvenanceV0,
809}
810
811#[allow(deprecated)]
812#[derive(Debug, Clone, PartialEq, Eq, Serialize)]
813#[serde(rename_all = "camelCase")]
814pub struct OmenaQueryUnifiedCrossFileHypergraphV0 {
815 pub schema_version: &'static str,
816 pub product: &'static str,
817 pub status: &'static str,
818 pub layer_marker: &'static str,
819 pub feature_gate: &'static str,
820 pub node_count: usize,
821 pub hyperedge_count: usize,
822 pub summary_edge_count: usize,
823 pub projection_edge_ids: Vec<String>,
824 pub hyperedges: Vec<UnifiedHypergraphHyperedgeV0>,
825 #[deprecated(
828 since = "0.4.0",
829 note = "use OmenaQueryUnifiedCrossFileMonotoneFactPropagationHypergraphV0::summary_edges; removal is not before 1.0 and requires downstream migration plus zero audited non-compatibility uses"
830 )]
831 pub summary_edges: Vec<HypergraphIFDSSummaryEdgeV0>,
832 pub gate_predicates: Vec<&'static str>,
833}
834
835#[derive(Debug, Clone, PartialEq, Eq, Serialize)]
836#[serde(rename_all = "camelCase")]
837pub struct OmenaQueryUnifiedCrossFileMonotoneFactPropagationHypergraphV0 {
838 pub schema_version: &'static str,
839 pub product: &'static str,
840 pub status: &'static str,
841 pub layer_marker: &'static str,
842 pub feature_gate: &'static str,
843 pub node_count: usize,
844 pub hyperedge_count: usize,
845 pub summary_edge_count: usize,
846 pub projection_edge_ids: Vec<String>,
847 pub hyperedges: Vec<UnifiedHypergraphHyperedgeV0>,
848 pub summary_edges: Vec<HypergraphMonotoneFactPropagationSummaryEdgeV0>,
849 pub gate_predicates: Vec<&'static str>,
850}
851
852#[derive(Debug, Clone, PartialEq, Eq, Serialize)]
853#[serde(rename_all = "camelCase")]
854pub struct OmenaQueryCrossFileSccEvidenceV0 {
855 pub schema_version: &'static str,
856 pub product: &'static str,
857 pub feature_gate: &'static str,
858 pub claim_level: &'static str,
859 pub theorem_claimed: bool,
860 pub connectivity_backend: &'static str,
861 pub polylog_bound_scope: &'static str,
862 pub scc_id: String,
863 pub node_count: usize,
864 pub directed_edge_count: usize,
865 pub cross_file: bool,
866 pub node_ids: Vec<String>,
867 pub style_paths: Vec<String>,
868 pub edge_kinds: Vec<&'static str>,
869 pub summary_edge_ids: Vec<String>,
870}
871
872#[derive(Debug, Clone, PartialEq, Eq, Serialize)]
873#[serde(rename_all = "camelCase")]
874pub struct OmenaQueryUnifiedCrossFileSccReportV0 {
875 pub schema_version: &'static str,
876 pub product: &'static str,
877 pub feature_gate: &'static str,
878 pub claim_level: &'static str,
879 pub theorem_claimed: bool,
880 pub connectivity_backend: &'static str,
881 pub polylog_bound_scope: &'static str,
882 pub node_count: usize,
883 pub directed_edge_count: usize,
884 pub cyclic_scc_count: usize,
885 pub sccs: Vec<OmenaQueryCrossFileSccEvidenceV0>,
886 pub gate_predicates: Vec<&'static str>,
887}
888
889#[derive(Debug, Clone, PartialEq, Eq)]
890pub struct HypergraphClosurePath<N> {
891 pub origin: N,
892 pub target: N,
893 pub depth: usize,
894 pub path_labels: Vec<String>,
895}
896
897#[derive(Debug, Clone, Copy, PartialEq, Eq)]
898pub enum HypergraphClosureMode {
899 CanonicalFirstTarget,
900 RawAllPaths,
901}
902
903pub trait OmenaUnifiedHypergraphConnectivityOracle {
904 fn reachable_node_ids(
905 &self,
906 start_node_id: &str,
907 hyperedges: &[UnifiedHypergraphHyperedgeV0],
908 ) -> Vec<String>;
909}
910
911#[derive(Debug, Clone, Copy, Default)]
912pub struct BatchHypergraphConnectivityOracle;
913
914impl OmenaUnifiedHypergraphConnectivityOracle for BatchHypergraphConnectivityOracle {
915 fn reachable_node_ids(
916 &self,
917 start_node_id: &str,
918 hyperedges: &[UnifiedHypergraphHyperedgeV0],
919 ) -> Vec<String> {
920 collect_reachable_node_ids(start_node_id, &build_adjacency(hyperedges))
921 }
922}
923
924pub fn collect_reachable_node_ids<K>(
931 start_node_id: &str,
932 adjacency: &BTreeMap<K, BTreeSet<String>>,
933) -> Vec<String>
934where
935 K: Ord + std::borrow::Borrow<str>,
936{
937 let mut seen = BTreeSet::new();
938 let mut pending = VecDeque::from([start_node_id.to_string()]);
939 while let Some(current) = pending.pop_front() {
940 for target in adjacency.get(current.as_str()).into_iter().flatten() {
941 if seen.insert(target.clone()) {
942 pending.push_back(target.clone());
943 }
944 }
945 }
946 seen.into_iter().collect()
947}
948
949pub fn collect_reachable_node_ids_bitset<K>(
950 start_node_id: &str,
951 adjacency: &BTreeMap<K, BTreeSet<String>>,
952) -> Vec<String>
953where
954 K: Ord + std::borrow::Borrow<str>,
955{
956 let dense_index = DenseNodeIndexV0::from_adjacency(start_node_id, adjacency);
957 let Some(start_index) = dense_index.index_of(start_node_id) else {
958 return Vec::new();
959 };
960 let dense_adjacency = dense_index.dense_adjacency(adjacency);
961 let mut seen = DenseBitsetV0::new(dense_index.len());
962 let mut pending = VecDeque::from([start_index]);
963 while let Some(current) = pending.pop_front() {
964 for target in dense_adjacency
965 .get(current)
966 .into_iter()
967 .flat_map(|targets| targets.iter().copied())
968 {
969 if seen.insert(target) {
970 pending.push_back(target);
971 }
972 }
973 }
974 dense_index.ids_for_bitset(&seen)
975}
976
977#[derive(Debug, Clone)]
978struct DenseNodeIndexV0 {
979 ids: Vec<String>,
980 positions: BTreeMap<String, usize>,
981}
982
983impl DenseNodeIndexV0 {
984 fn from_adjacency<K>(start_node_id: &str, adjacency: &BTreeMap<K, BTreeSet<String>>) -> Self
985 where
986 K: Ord + std::borrow::Borrow<str>,
987 {
988 let mut ids = BTreeSet::from([start_node_id.to_string()]);
989 for (source, targets) in adjacency {
990 ids.insert(source.borrow().to_string());
991 ids.extend(targets.iter().cloned());
992 }
993 let ids = ids.into_iter().collect::<Vec<_>>();
994 let positions = ids
995 .iter()
996 .enumerate()
997 .map(|(index, node_id)| (node_id.clone(), index))
998 .collect::<BTreeMap<_, _>>();
999 Self { ids, positions }
1000 }
1001
1002 fn len(&self) -> usize {
1003 self.ids.len()
1004 }
1005
1006 fn index_of(&self, node_id: &str) -> Option<usize> {
1007 self.positions.get(node_id).copied()
1008 }
1009
1010 fn dense_adjacency<K>(&self, adjacency: &BTreeMap<K, BTreeSet<String>>) -> Vec<Vec<usize>>
1011 where
1012 K: Ord + std::borrow::Borrow<str>,
1013 {
1014 let mut dense_adjacency = vec![Vec::new(); self.ids.len()];
1015 for (source, targets) in adjacency {
1016 let Some(source_index) = self.index_of(source.borrow()) else {
1017 continue;
1018 };
1019 dense_adjacency[source_index] = targets
1020 .iter()
1021 .filter_map(|target| self.index_of(target))
1022 .collect();
1023 }
1024 dense_adjacency
1025 }
1026
1027 fn ids_for_bitset(&self, bitset: &DenseBitsetV0) -> Vec<String> {
1028 self.ids
1029 .iter()
1030 .enumerate()
1031 .filter(|(index, _)| bitset.contains(*index))
1032 .map(|(_, node_id)| node_id.clone())
1033 .collect()
1034 }
1035}
1036
1037#[derive(Debug, Clone)]
1038struct DenseBitsetV0 {
1039 words: Vec<u64>,
1040}
1041
1042impl DenseBitsetV0 {
1043 fn new(len: usize) -> Self {
1044 Self {
1045 words: vec![0; len.div_ceil(64)],
1046 }
1047 }
1048
1049 fn insert(&mut self, index: usize) -> bool {
1050 let word_index = index / 64;
1051 let mask = 1u64 << (index % 64);
1052 let word = &mut self.words[word_index];
1053 let was_empty = *word & mask == 0;
1054 *word |= mask;
1055 was_empty
1056 }
1057
1058 fn contains(&self, index: usize) -> bool {
1059 self.words
1060 .get(index / 64)
1061 .is_some_and(|word| word & (1u64 << (index % 64)) != 0)
1062 }
1063}
1064
1065pub fn tabulate_hypergraph_monotone_fact_propagation_summary_edges(
1066 hyperedges: &[UnifiedHypergraphHyperedgeV0],
1067 projected_edges: Vec<HypergraphMonotoneFactPropagationSummaryEdgeV0>,
1068) -> Vec<HypergraphMonotoneFactPropagationSummaryEdgeV0> {
1069 let hyperedge_ids = hyperedges
1070 .iter()
1071 .map(|edge| edge.hyperedge_id.as_str())
1072 .collect::<BTreeSet<_>>();
1073 let mut edges = projected_edges
1074 .into_iter()
1075 .filter(|edge| hyperedge_ids.contains(edge.hyperedge_id.as_str()))
1076 .collect::<Vec<_>>();
1077 edges.sort_by(|left, right| {
1078 left.projection_edge_id
1079 .cmp(&right.projection_edge_id)
1080 .then(left.hyperedge_id.cmp(&right.hyperedge_id))
1081 });
1082 edges
1083}
1084
1085#[allow(deprecated)]
1086#[deprecated(
1087 since = "0.4.0",
1088 note = "compatibility conversion owned by omena-cross-file-summary maintainers; removal is not before 1.0 and requires downstream migration plus zero audited non-compatibility uses"
1089)]
1090fn hypergraph_summary_edge_into_compatibility_wire_v0(
1091 edge: HypergraphMonotoneFactPropagationSummaryEdgeV0,
1092) -> HypergraphIFDSSummaryEdgeV0 {
1093 HypergraphIFDSSummaryEdgeV0 {
1094 schema_version: edge.schema_version,
1095 product: "omena-query.hypergraph-ifds-summary-edge",
1096 layer_marker: "hypergraph-ifds",
1097 feature_gate: "hypergraph-ifds",
1098 summary_edge_id: edge.summary_edge_id.replacen(
1099 "monotone-fact-propagation-summary:",
1100 "ifds-summary:",
1101 1,
1102 ),
1103 projection_edge_id: edge.projection_edge_id,
1104 hyperedge_id: edge.hyperedge_id,
1105 from_node_id: edge.from_node_id,
1106 to_node_id: edge.to_node_id,
1107 edge_kind: edge.edge_kind,
1108 status: edge.status,
1109 provenance: edge.provenance,
1110 linear_provenance: edge.linear_provenance,
1111 }
1112}
1113
1114#[allow(deprecated)]
1115#[deprecated(
1116 since = "0.4.0",
1117 note = "compatibility conversion owned by omena-cross-file-summary maintainers; removal is not before 1.0 and requires downstream migration plus zero audited non-compatibility uses"
1118)]
1119fn hypergraph_summary_edge_into_canonical_v0(
1120 edge: HypergraphIFDSSummaryEdgeV0,
1121) -> HypergraphMonotoneFactPropagationSummaryEdgeV0 {
1122 HypergraphMonotoneFactPropagationSummaryEdgeV0 {
1123 schema_version: edge.schema_version,
1124 product: "omena-query.hypergraph-monotone-fact-propagation-summary-edge",
1125 layer_marker: "hypergraph-monotone-fact-propagation",
1126 feature_gate: "hypergraph-monotone-fact-propagation",
1127 summary_edge_id: edge.summary_edge_id.replacen(
1128 "ifds-summary:",
1129 "monotone-fact-propagation-summary:",
1130 1,
1131 ),
1132 projection_edge_id: edge.projection_edge_id,
1133 hyperedge_id: edge.hyperedge_id,
1134 from_node_id: edge.from_node_id,
1135 to_node_id: edge.to_node_id,
1136 edge_kind: edge.edge_kind,
1137 status: edge.status,
1138 provenance: edge.provenance,
1139 linear_provenance: edge.linear_provenance,
1140 }
1141}
1142
1143#[allow(deprecated)]
1146#[deprecated(
1147 since = "0.4.0",
1148 note = "use tabulate_hypergraph_monotone_fact_propagation_summary_edges; removal is not before 1.0 and requires downstream migration plus zero audited non-compatibility uses"
1149)]
1150pub fn tabulate_hypergraph_ifds_summary_edges(
1151 hyperedges: &[UnifiedHypergraphHyperedgeV0],
1152 projected_edges: Vec<HypergraphIFDSSummaryEdgeV0>,
1153) -> Vec<HypergraphIFDSSummaryEdgeV0> {
1154 tabulate_hypergraph_monotone_fact_propagation_summary_edges(
1155 hyperedges,
1156 projected_edges
1157 .into_iter()
1158 .map(hypergraph_summary_edge_into_canonical_v0)
1159 .collect(),
1160 )
1161 .into_iter()
1162 .map(hypergraph_summary_edge_into_compatibility_wire_v0)
1163 .collect()
1164}
1165
1166pub fn summarize_omena_query_unified_cross_file_scc_report(
1167 hypergraph: &OmenaQueryUnifiedCrossFileHypergraphV0,
1168) -> OmenaQueryUnifiedCrossFileSccReportV0 {
1169 #[allow(deprecated)]
1170 let canonical_summary_edges = hypergraph
1171 .summary_edges
1172 .iter()
1173 .cloned()
1174 .map(hypergraph_summary_edge_into_canonical_v0)
1175 .collect::<Vec<_>>();
1176 let adjacency = build_directed_projection_adjacency(&canonical_summary_edges);
1177 let mut sccs = collect_directed_graph_sccs(&adjacency)
1178 .into_iter()
1179 .filter_map(|node_ids| summarize_cyclic_scc(&node_ids, &canonical_summary_edges))
1180 .collect::<Vec<_>>();
1181 sccs.sort_by(|left, right| {
1182 left.node_ids
1183 .cmp(&right.node_ids)
1184 .then(left.summary_edge_ids.cmp(&right.summary_edge_ids))
1185 });
1186 for (index, scc) in sccs.iter_mut().enumerate() {
1187 scc.scc_id = format!("exact-tarjan-scc:{}", index + 1);
1188 }
1189
1190 OmenaQueryUnifiedCrossFileSccReportV0 {
1191 schema_version: "0",
1192 product: "omena-query.unified-cross-file-scc-report",
1193 feature_gate: "cross-file-scc-v0",
1194 claim_level: "fixtureWitnessExactTarjanScc",
1195 theorem_claimed: false,
1196 connectivity_backend: "exactTarjanScc",
1197 polylog_bound_scope: "notClaimedExactTraversal",
1198 node_count: adjacency.len(),
1199 directed_edge_count: canonical_summary_edges
1200 .iter()
1201 .filter(|edge| summary_edge_has_supported_target(edge.status))
1202 .count(),
1203 cyclic_scc_count: sccs.len(),
1204 sccs,
1205 gate_predicates: vec![
1206 "exactTarjanSccBackend",
1207 "theorem_claimed=false",
1208 "polylog_bound_scope=notClaimedExactTraversal",
1209 ],
1210 }
1211}
1212
1213#[allow(deprecated)]
1214pub fn summarize_omena_query_unified_cross_file_hypergraph(
1215 summary: &OmenaQueryCrossFileSummaryV0,
1216) -> OmenaQueryUnifiedCrossFileHypergraphV0 {
1217 hypergraph_into_compatibility_wire_v0(
1218 summarize_omena_query_unified_cross_file_monotone_fact_propagation_hypergraph(summary),
1219 )
1220}
1221
1222pub fn summarize_omena_query_unified_cross_file_monotone_fact_propagation_hypergraph(
1223 summary: &OmenaQueryCrossFileSummaryV0,
1224) -> OmenaQueryUnifiedCrossFileMonotoneFactPropagationHypergraphV0 {
1225 let mut builder = UnifiedCrossFileHypergraphBuilder::default();
1226 for edge in &summary.edges {
1227 builder.add_summary_edge(edge);
1228 }
1229 builder.finish()
1230}
1231
1232#[allow(deprecated)]
1233#[deprecated(
1234 since = "0.4.0",
1235 note = "compatibility conversion owned by omena-cross-file-summary maintainers; removal is not before 1.0 and requires downstream migration plus zero audited non-compatibility uses"
1236)]
1237fn hypergraph_into_compatibility_wire_v0(
1238 hypergraph: OmenaQueryUnifiedCrossFileMonotoneFactPropagationHypergraphV0,
1239) -> OmenaQueryUnifiedCrossFileHypergraphV0 {
1240 let hyperedges = hypergraph
1241 .hyperedges
1242 .into_iter()
1243 .map(|mut edge| {
1244 edge.layer_marker = "hypergraph-ifds";
1245 edge.feature_gate = "hypergraph-ifds";
1246 edge
1247 })
1248 .collect();
1249 let summary_edges = hypergraph
1250 .summary_edges
1251 .into_iter()
1252 .map(hypergraph_summary_edge_into_compatibility_wire_v0)
1253 .collect();
1254 OmenaQueryUnifiedCrossFileHypergraphV0 {
1255 schema_version: hypergraph.schema_version,
1256 product: hypergraph.product,
1257 status: "hypergraphIfdsProjection",
1258 layer_marker: "hypergraph-ifds",
1259 feature_gate: "hypergraph-ifds",
1260 node_count: hypergraph.node_count,
1261 hyperedge_count: hypergraph.hyperedge_count,
1262 summary_edge_count: hypergraph.summary_edge_count,
1263 projection_edge_ids: hypergraph.projection_edge_ids,
1264 hyperedges,
1265 summary_edges,
1266 gate_predicates: hypergraph.gate_predicates,
1267 }
1268}
1269
1270pub fn collect_hypergraph_transitive_closure_paths<N, F>(
1271 graph: &BTreeMap<N, BTreeSet<N>>,
1272 mut label: F,
1273) -> (Vec<HypergraphClosurePath<N>>, Vec<Vec<String>>)
1274where
1275 N: Clone + Ord,
1276 F: FnMut(&N) -> String,
1277{
1278 collect_hypergraph_transitive_closure_paths_with_mode(
1279 graph,
1280 &mut label,
1281 HypergraphClosureMode::CanonicalFirstTarget,
1282 )
1283}
1284
1285pub fn collect_hypergraph_transitive_closure_paths_with_mode<N, F>(
1286 graph: &BTreeMap<N, BTreeSet<N>>,
1287 label: &mut F,
1288 mode: HypergraphClosureMode,
1289) -> (Vec<HypergraphClosurePath<N>>, Vec<Vec<String>>)
1290where
1291 N: Clone + Ord,
1292 F: FnMut(&N) -> String,
1293{
1294 let mut closure_paths = Vec::new();
1295 let mut cycle_paths = Vec::new();
1296 let mut seen_cycles = BTreeSet::new();
1297 let first_target = mode == HypergraphClosureMode::CanonicalFirstTarget;
1298
1299 for start in graph.keys() {
1300 let mut visited = BTreeSet::new();
1301 let mut pending = VecDeque::from([(start.clone(), vec![start.clone()])]);
1302 while let Some((current, path)) = pending.pop_front() {
1303 for target in graph.get(¤t).into_iter().flatten() {
1304 if let Some(cycle_start) = path.iter().position(|node| node == target) {
1305 let mut cycle = path[cycle_start..].to_vec();
1306 cycle.push(target.clone());
1307 let mut labels = cycle.iter().map(&mut *label).collect::<Vec<_>>();
1308 if first_target {
1309 labels = canonical_hypergraph_cycle_labels(labels);
1310 }
1311 if !labels.is_empty() && seen_cycles.insert(labels.clone()) {
1312 cycle_paths.push(labels);
1313 }
1314 continue;
1315 }
1316 if first_target && !visited.insert(target.clone()) {
1317 continue;
1318 }
1319 let mut edge_path = path.clone();
1320 edge_path.push(target.clone());
1321 closure_paths.push(HypergraphClosurePath {
1322 origin: start.clone(),
1323 target: target.clone(),
1324 depth: edge_path.len().saturating_sub(1),
1325 path_labels: edge_path.iter().map(&mut *label).collect(),
1326 });
1327 pending.push_back((target.clone(), edge_path));
1328 }
1329 }
1330 }
1331 (closure_paths, cycle_paths)
1332}
1333
1334const DEFAULT_CYCLE_ENUMERATION_WORK_CAP: usize = 1 << 16;
1337
1338pub fn collect_directed_graph_cycles(
1345 adjacency: &BTreeMap<String, BTreeSet<String>>,
1346) -> Vec<Vec<String>> {
1347 collect_directed_graph_cycles_with_work_cap(adjacency, DEFAULT_CYCLE_ENUMERATION_WORK_CAP)
1348}
1349
1350pub fn collect_directed_graph_cycles_with_work_cap(
1354 adjacency: &BTreeMap<String, BTreeSet<String>>,
1355 per_scc_work_cap: usize,
1356) -> Vec<Vec<String>> {
1357 let mut circuits = BTreeSet::new();
1358 for scc in collect_directed_graph_sccs(adjacency) {
1359 let self_loop = scc.len() == 1
1360 && adjacency
1361 .get(scc[0].as_str())
1362 .is_some_and(|targets| targets.contains(&scc[0]));
1363 if scc.len() < 2 && !self_loop {
1364 continue;
1365 }
1366 let scc_nodes = scc.iter().map(String::as_str).collect::<BTreeSet<_>>();
1367 let mut found = BTreeSet::new();
1368 let mut work = 0usize;
1369 let mut capped = false;
1370 'starts: for start in &scc {
1371 let mut pending = VecDeque::from([(start.clone(), vec![start.clone()])]);
1374 while let Some((current, path)) = pending.pop_front() {
1375 work += 1;
1376 if work > per_scc_work_cap {
1377 capped = true;
1378 break 'starts;
1379 }
1380 for target in adjacency.get(current.as_str()).into_iter().flatten() {
1381 if !scc_nodes.contains(target.as_str()) {
1382 continue;
1383 }
1384 if target == start {
1385 let mut ring = path.clone();
1386 ring.push(target.clone());
1387 let canonical = canonical_hypergraph_cycle_labels(ring);
1388 if !canonical.is_empty() {
1389 found.insert(canonical);
1390 }
1391 } else if !path.iter().any(|node| node == target) {
1392 let mut next = path.clone();
1393 next.push(target.clone());
1394 pending.push_back((target.clone(), next));
1395 }
1396 }
1397 }
1398 }
1399 if capped {
1400 circuits.extend(found.into_iter().next());
1401 } else {
1402 circuits.extend(found);
1403 }
1404 }
1405 circuits.into_iter().collect()
1406}
1407
1408fn canonical_hypergraph_cycle_labels(mut labels: Vec<String>) -> Vec<String> {
1409 if labels.len() > 1 && labels.first() == labels.last() {
1410 labels.pop();
1411 }
1412 if labels.is_empty() {
1413 return labels;
1414 }
1415 let mut best = labels.clone();
1416 for offset in 1..labels.len() {
1417 let mut rotated = labels[offset..].to_vec();
1418 rotated.extend_from_slice(&labels[..offset]);
1419 best = best.min(rotated);
1420 }
1421 best.push(best[0].clone());
1422 best
1423}
1424
1425fn build_adjacency(
1426 hyperedges: &[UnifiedHypergraphHyperedgeV0],
1427) -> BTreeMap<&str, BTreeSet<String>> {
1428 let mut adjacency = BTreeMap::<&str, BTreeSet<String>>::new();
1429 for edge in hyperedges {
1430 for tail in &edge.tail_node_ids {
1431 adjacency
1432 .entry(tail.as_str())
1433 .or_default()
1434 .insert(edge.head_node_id.clone());
1435 }
1436 }
1437 adjacency
1438}
1439
1440#[derive(Default)]
1441struct UnifiedCrossFileHypergraphBuilder {
1442 node_ids: BTreeSet<String>,
1443 hyperedges: Vec<UnifiedHypergraphHyperedgeV0>,
1444 summary_edges: Vec<HypergraphMonotoneFactPropagationSummaryEdgeV0>,
1445}
1446
1447impl UnifiedCrossFileHypergraphBuilder {
1448 fn add_summary_edge(&mut self, edge: &OmenaQueryCrossFileSummaryEdgeV0) {
1449 let edge_kind = unified_edge_kind_for_summary_edge(edge);
1450 let from_node_id = endpoint_node_id(edge, false);
1451 let to_node_id = endpoint_node_id(edge, true);
1452 let tail_node_ids = if edge_kind.is_order_significant() && !edge.target_names.is_empty() {
1453 edge.target_names
1454 .iter()
1455 .map(|target_name| {
1456 node_id(
1457 "styleSymbol",
1458 edge.target_path
1459 .as_deref()
1460 .unwrap_or(edge.from_path.as_str()),
1461 Some(target_name),
1462 )
1463 })
1464 .collect::<Vec<_>>()
1465 } else {
1466 vec![from_node_id.clone()]
1467 };
1468 self.node_ids.insert(from_node_id.clone());
1469 self.node_ids.insert(to_node_id.clone());
1470 self.node_ids.extend(tail_node_ids.iter().cloned());
1471
1472 let hyperedge_id = format!(
1473 "hyperedge:{}|{}|{}",
1474 edge_kind.as_wire_label(),
1475 edge.edge_id,
1476 tail_node_ids.join(">")
1477 );
1478 self.hyperedges.push(UnifiedHypergraphHyperedgeV0 {
1479 schema_version: "0",
1480 product: "omena-query.unified-hypergraph-hyperedge",
1481 layer_marker: "hypergraph-monotone-fact-propagation",
1482 feature_gate: "hypergraph-monotone-fact-propagation",
1483 hyperedge_id: hyperedge_id.clone(),
1484 edge_kind,
1485 source_summary_edge_id: edge.edge_id.clone(),
1486 source_edge_kind: edge.edge_kind,
1487 source_status: edge.status,
1488 tail_node_ids,
1489 head_node_id: to_node_id.clone(),
1490 order_significant_tail: edge_kind.is_order_significant(),
1491 });
1492 self.summary_edges
1493 .push(HypergraphMonotoneFactPropagationSummaryEdgeV0 {
1494 schema_version: "0",
1495 product: "omena-query.hypergraph-monotone-fact-propagation-summary-edge",
1496 layer_marker: "hypergraph-monotone-fact-propagation",
1497 feature_gate: "hypergraph-monotone-fact-propagation",
1498 summary_edge_id: format!("monotone-fact-propagation-summary:{}", edge.edge_id),
1499 projection_edge_id: edge.edge_id.clone(),
1500 hyperedge_id,
1501 from_node_id,
1502 to_node_id,
1503 edge_kind,
1504 status: edge.status,
1505 provenance: edge.provenance.clone(),
1506 linear_provenance: edge.linear_provenance.clone(),
1507 });
1508 }
1509
1510 fn finish(mut self) -> OmenaQueryUnifiedCrossFileMonotoneFactPropagationHypergraphV0 {
1511 self.hyperedges
1512 .sort_by_key(|edge| edge.hyperedge_id.clone());
1513 let summary_edges = tabulate_hypergraph_monotone_fact_propagation_summary_edges(
1514 &self.hyperedges,
1515 self.summary_edges,
1516 );
1517 let projection_edge_ids = summary_edges
1518 .iter()
1519 .map(|edge| edge.projection_edge_id.clone())
1520 .collect::<Vec<_>>();
1521
1522 OmenaQueryUnifiedCrossFileMonotoneFactPropagationHypergraphV0 {
1523 schema_version: "0",
1524 product: "omena-query.unified-cross-file-hypergraph",
1525 status: "hypergraphMonotoneFactPropagationProjection",
1526 layer_marker: "hypergraph-monotone-fact-propagation",
1527 feature_gate: "hypergraph-monotone-fact-propagation",
1528 node_count: self.node_ids.len(),
1529 hyperedge_count: self.hyperedges.len(),
1530 summary_edge_count: summary_edges.len(),
1531 projection_edge_ids,
1532 hyperedges: self.hyperedges,
1533 summary_edges,
1534 gate_predicates: vec![
1535 "P1.typeIntroduction",
1536 "P2.byteEqualAdjacencyProjection",
1537 "P3.sccUnification",
1538 "P4.summaryEdgeSetEquality",
1539 "P5.projectionHelper",
1540 "P6.closureBodySwitchOver",
1541 "P7.v0Publication",
1542 "batchConnectivityOracle",
1543 "streamingOracleWireCompatible",
1544 "composesTailOrderingUsesVec",
1545 ],
1546 }
1547 }
1548}
1549
1550fn endpoint_node_id(edge: &OmenaQueryCrossFileSummaryEdgeV0, target: bool) -> String {
1551 let (kind, path, symbol) = if target {
1552 (
1553 node_kind_for_summary_kind(edge.target_kind.unwrap_or(edge.from_kind), true),
1554 edge.target_path
1555 .as_deref()
1556 .unwrap_or(edge.from_path.as_str()),
1557 edge.remote_name
1558 .as_deref()
1559 .or_else(|| edge.target_names.first().map(String::as_str)),
1560 )
1561 } else {
1562 (
1563 node_kind_for_summary_kind(edge.from_kind, false),
1564 edge.from_path.as_str(),
1565 edge.owner_selector_name
1566 .as_deref()
1567 .or(edge.local_name.as_deref()),
1568 )
1569 };
1570 node_id(kind, path, symbol)
1571}
1572
1573fn node_id(kind: &'static str, path: &str, symbol: Option<&str>) -> String {
1574 format!("{}|{}|{}", kind, path, symbol.unwrap_or("-"))
1575}
1576
1577fn node_kind_for_summary_kind(kind: &str, target: bool) -> &'static str {
1578 match (kind, target) {
1579 ("style", false) => "styleModule",
1580 ("style", true) => "styleSymbol",
1581 ("source", false) => "sourceModule",
1582 ("source", true) => "sourceSymbol",
1583 _ => "foreignSymbol",
1584 }
1585}
1586
1587fn unified_edge_kind_for_summary_edge(
1588 edge: &OmenaQueryCrossFileSummaryEdgeV0,
1589) -> UnifiedHypergraphEdgeKindV0 {
1590 match edge.edge_kind {
1591 "composesLocal" => UnifiedHypergraphEdgeKindV0::ComposesLocal,
1592 "composesGlobal" => UnifiedHypergraphEdgeKindV0::ComposesGlobal,
1593 "cssModulesComposesImport" | "cssModulesComposesClosure" | "composesExternal" => {
1594 UnifiedHypergraphEdgeKindV0::ComposesExternal
1595 }
1596 "sassUse" => UnifiedHypergraphEdgeKindV0::SassUse,
1597 "sassForward" => UnifiedHypergraphEdgeKindV0::SassForward,
1598 "sassImport" => UnifiedHypergraphEdgeKindV0::SassImport,
1599 "lessImport" => UnifiedHypergraphEdgeKindV0::LessImport,
1600 "lessModuleGraphClosure" => UnifiedHypergraphEdgeKindV0::LessModuleGraphClosure,
1601 "cssModulesValueImport" | "cssModulesValueClosure" | "value" => {
1602 UnifiedHypergraphEdgeKindV0::Value
1603 }
1604 "cssModulesIcssImport" | "cssModulesIcssClosure" | "icss" => {
1605 UnifiedHypergraphEdgeKindV0::Icss
1606 }
1607 _ => UnifiedHypergraphEdgeKindV0::ForeignReference,
1608 }
1609}
1610
1611fn build_directed_projection_adjacency(
1612 summary_edges: &[HypergraphMonotoneFactPropagationSummaryEdgeV0],
1613) -> BTreeMap<String, BTreeSet<String>> {
1614 let mut adjacency = BTreeMap::<String, BTreeSet<String>>::new();
1615 for edge in summary_edges {
1616 if !summary_edge_has_supported_target(edge.status) {
1617 continue;
1618 }
1619 let from_node_id = canonical_scc_node_id(edge.from_node_id.as_str());
1620 let to_node_id = canonical_scc_node_id(edge.to_node_id.as_str());
1621 adjacency.entry(from_node_id.clone()).or_default();
1622 adjacency.entry(to_node_id.clone()).or_default();
1623 adjacency
1624 .entry(from_node_id)
1625 .or_default()
1626 .insert(to_node_id);
1627 }
1628 adjacency
1629}
1630
1631pub fn collect_directed_graph_sccs(
1636 adjacency: &BTreeMap<String, BTreeSet<String>>,
1637) -> Vec<Vec<String>> {
1638 let mut state = TarjanState::default();
1639 for node_id in adjacency.keys() {
1640 if !state.indices.contains_key(node_id) {
1641 state.visit(node_id, adjacency);
1642 }
1643 }
1644 state.components
1645}
1646
1647#[derive(Default)]
1648struct TarjanState {
1649 next_index: usize,
1650 stack: Vec<String>,
1651 on_stack: BTreeSet<String>,
1652 indices: BTreeMap<String, usize>,
1653 lowlinks: BTreeMap<String, usize>,
1654 components: Vec<Vec<String>>,
1655}
1656
1657impl TarjanState {
1658 fn visit(&mut self, node_id: &str, adjacency: &BTreeMap<String, BTreeSet<String>>) {
1659 let index = self.next_index;
1660 self.next_index += 1;
1661 self.indices.insert(node_id.to_string(), index);
1662 self.lowlinks.insert(node_id.to_string(), index);
1663 self.stack.push(node_id.to_string());
1664 self.on_stack.insert(node_id.to_string());
1665
1666 if let Some(targets) = adjacency.get(node_id) {
1667 for target in targets {
1668 if !self.indices.contains_key(target.as_str()) {
1669 self.visit(target, adjacency);
1670 let target_lowlink = self.lowlinks[target.as_str()];
1671 let current_lowlink = self.lowlinks[node_id];
1672 self.lowlinks
1673 .insert(node_id.to_string(), current_lowlink.min(target_lowlink));
1674 } else if self.on_stack.contains(target.as_str()) {
1675 let target_index = self.indices[target.as_str()];
1676 let current_lowlink = self.lowlinks[node_id];
1677 self.lowlinks
1678 .insert(node_id.to_string(), current_lowlink.min(target_index));
1679 }
1680 }
1681 }
1682
1683 if self.lowlinks[node_id] == self.indices[node_id] {
1684 let mut component = Vec::new();
1685 while let Some(stack_node) = self.stack.pop() {
1686 self.on_stack.remove(stack_node.as_str());
1687 let done = stack_node == node_id;
1688 component.push(stack_node);
1689 if done {
1690 break;
1691 }
1692 }
1693 component.sort();
1694 self.components.push(component);
1695 }
1696 }
1697}
1698
1699fn summarize_cyclic_scc(
1700 node_ids: &[String],
1701 summary_edges: &[HypergraphMonotoneFactPropagationSummaryEdgeV0],
1702) -> Option<OmenaQueryCrossFileSccEvidenceV0> {
1703 let node_set = node_ids.iter().map(String::as_str).collect::<BTreeSet<_>>();
1704 let internal_edges = summary_edges
1705 .iter()
1706 .filter(|edge| summary_edge_has_supported_target(edge.status))
1707 .filter(|edge| {
1708 let from_node_id = canonical_scc_node_id(edge.from_node_id.as_str());
1709 let to_node_id = canonical_scc_node_id(edge.to_node_id.as_str());
1710 node_set.contains(from_node_id.as_str()) && node_set.contains(to_node_id.as_str())
1711 })
1712 .collect::<Vec<_>>();
1713 let has_self_loop = internal_edges.iter().any(|edge| {
1714 canonical_scc_node_id(edge.from_node_id.as_str())
1715 == canonical_scc_node_id(edge.to_node_id.as_str())
1716 });
1717 if node_ids.len() < 2 && !has_self_loop {
1718 return None;
1719 }
1720
1721 let style_paths = node_ids
1722 .iter()
1723 .filter_map(|node_id| style_path_from_node_id(node_id))
1724 .collect::<BTreeSet<_>>()
1725 .into_iter()
1726 .collect::<Vec<_>>();
1727 let edge_kinds = internal_edges
1728 .iter()
1729 .map(|edge| edge.edge_kind.as_wire_label())
1730 .collect::<BTreeSet<_>>()
1731 .into_iter()
1732 .collect::<Vec<_>>();
1733 let summary_edge_ids = internal_edges
1734 .iter()
1735 .map(|edge| edge.projection_edge_id.clone())
1736 .collect::<BTreeSet<_>>()
1737 .into_iter()
1738 .collect::<Vec<_>>();
1739
1740 Some(OmenaQueryCrossFileSccEvidenceV0 {
1741 schema_version: "0",
1742 product: "omena-query.cross-file-scc-evidence",
1743 feature_gate: "cross-file-scc-v0",
1744 claim_level: "fixtureWitnessExactTarjanScc",
1745 theorem_claimed: false,
1746 connectivity_backend: "exactTarjanScc",
1747 polylog_bound_scope: "notClaimedExactTraversal",
1748 scc_id: String::new(),
1749 node_count: node_ids.len(),
1750 directed_edge_count: internal_edges.len(),
1751 cross_file: style_paths.len() > 1,
1752 node_ids: node_ids.to_vec(),
1753 style_paths,
1754 edge_kinds,
1755 summary_edge_ids,
1756 })
1757}
1758
1759fn style_path_from_node_id(node_id: &str) -> Option<String> {
1760 let mut parts = node_id.splitn(3, '|');
1761 let _kind = parts.next()?;
1762 let path = parts.next()?;
1763 Some(path.to_string())
1764}
1765
1766fn canonical_scc_node_id(node_id: &str) -> String {
1767 let mut parts = node_id.splitn(3, '|');
1768 let Some(kind) = parts.next() else {
1769 return node_id.to_string();
1770 };
1771 let Some(path) = parts.next() else {
1772 return node_id.to_string();
1773 };
1774 let Some(symbol) = parts.next() else {
1775 return node_id.to_string();
1776 };
1777 if kind == "styleModule" && symbol != "-" {
1778 return format!("styleSymbol|{path}|{symbol}");
1779 }
1780 node_id.to_string()
1781}
1782
1783fn summary_edge_has_supported_target(status: &str) -> bool {
1784 matches!(
1785 status,
1786 "resolved" | "reachable" | "localResolved" | "importResolved" | "external"
1787 )
1788}
1789
1790fn stable_omena_query_cross_file_summary_hash(
1791 edges: &[OmenaQueryCrossFileSummaryEdgeV0],
1792) -> String {
1793 let mut hash = 0xcbf29ce484222325u64;
1794 stable_omena_query_hash_piece(&mut hash, "omena-query.cross-file-summary");
1795 stable_omena_query_hash_piece(&mut hash, "0");
1796 for edge in edges {
1797 stable_omena_query_hash_piece(&mut hash, edge.edge_id.as_str());
1798 stable_omena_query_hash_piece(&mut hash, edge.status);
1799 stable_omena_query_hash_piece(&mut hash, edge.linear_provenance.semiring_identifier());
1800 let term_count = edge.linear_provenance.term_count.to_string();
1801 stable_omena_query_hash_piece(&mut hash, term_count.as_str());
1802 for term in &edge.linear_provenance.terms {
1803 let coefficient = term.coefficient.to_string();
1804 stable_omena_query_hash_piece(&mut hash, coefficient.as_str());
1805 stable_omena_query_hash_piece(&mut hash, term.label);
1806 }
1807 }
1808 format!("{hash:016x}")
1809}
1810
1811fn stable_omena_query_hash_piece(hash: &mut u64, piece: &str) {
1812 for byte in piece.as_bytes() {
1813 *hash ^= u64::from(*byte);
1814 *hash = hash.wrapping_mul(0x100000001b3);
1815 }
1816 *hash ^= 0xff;
1817 *hash = hash.wrapping_mul(0x100000001b3);
1818}
1819
1820#[cfg(test)]
1821mod tests {
1822 use sha2::{Digest, Sha256};
1823
1824 use super::*;
1825
1826 #[test]
1827 #[allow(deprecated)]
1828 fn compatibility_and_canonical_hypergraphs_keep_distinct_exact_wire_bytes()
1829 -> Result<(), serde_json::Error> {
1830 let summary = summary_from_edges(vec![fixture_edge_with_kind(
1831 "sassUse",
1832 "style",
1833 "/workspace/a.scss",
1834 "style",
1835 "/workspace/b.scss",
1836 )]);
1837 let compatibility = summarize_omena_query_unified_cross_file_hypergraph(&summary);
1838 let canonical =
1839 summarize_omena_query_unified_cross_file_monotone_fact_propagation_hypergraph(&summary);
1840 let compatibility_json = serde_json::to_string(&compatibility)?;
1841 let canonical_json = serde_json::to_string(&canonical)?;
1842 let digest = |bytes: &[u8]| {
1843 Sha256::digest(bytes)
1844 .iter()
1845 .map(|byte| format!("{byte:02x}"))
1846 .collect::<String>()
1847 };
1848 assert_eq!(compatibility_json.len(), 2_109);
1849 assert_eq!(
1850 digest(compatibility_json.as_bytes()),
1851 "3e937371e24d00ee36cc10874fb2b9a84e2e4d3b341c352e9fac7e96057a94ee"
1852 );
1853 assert_eq!(canonical_json.len(), 2_296);
1854 assert_eq!(
1855 digest(canonical_json.as_bytes()),
1856 "1440656b0991e00dde7782e3cabc34037ce1eaf5ddaa91c3755f5d526fd08f12"
1857 );
1858 assert_ne!(compatibility_json, canonical_json);
1859 Ok(())
1860 }
1861
1862 #[test]
1863 fn bitset_reachability_matches_btreeset_closure_order_and_cycles() {
1864 let adjacency = BTreeMap::from([
1865 (
1866 "module:root".to_string(),
1867 BTreeSet::from(["symbol:base".to_string(), "symbol:theme".to_string()]),
1868 ),
1869 (
1870 "symbol:base".to_string(),
1871 BTreeSet::from(["symbol:terminal".to_string()]),
1872 ),
1873 (
1874 "symbol:theme".to_string(),
1875 BTreeSet::from(["module:root".to_string(), "symbol:terminal".to_string()]),
1876 ),
1877 ]);
1878
1879 let btreeset = collect_reachable_node_ids("module:root", &adjacency);
1880 let bitset = collect_reachable_node_ids_bitset("module:root", &adjacency);
1881
1882 assert_eq!(
1883 bitset,
1884 vec![
1885 "module:root".to_string(),
1886 "symbol:base".to_string(),
1887 "symbol:terminal".to_string(),
1888 "symbol:theme".to_string(),
1889 ]
1890 );
1891 assert_eq!(bitset, btreeset);
1892 }
1893
1894 #[test]
1895 fn reverse_dependency_delta_matches_from_scratch_index_across_edge_edits() {
1896 let sequences = reverse_dependency_edit_sequences();
1897 assert!(sequences.len() >= 12);
1898 assert!(
1899 sequences
1900 .iter()
1901 .any(|sequence| sequence_has_add_and_remove_for_same_origin(sequence)),
1902 "fixture corpus must include add/remove edits for an existing origin"
1903 );
1904
1905 for sequence in sequences {
1906 let mut incremental = reverse_dependency_index_from_edges_v0(&[]);
1907 let mut patched_total = 0;
1908 for next_edges in &sequence {
1909 patched_total += apply_reverse_dependency_delta_v0(&mut incremental, next_edges);
1910 }
1911 let final_edges = sequence.last().map(Vec::as_slice).unwrap_or(&[]);
1912 let from_scratch = reverse_dependency_index_from_edges_v0(final_edges);
1913
1914 assert_eq!(incremental, from_scratch);
1915 assert_eq!(
1916 reverse_dependency_index_fingerprint(&incremental),
1917 reverse_dependency_index_fingerprint(&from_scratch)
1918 );
1919 assert!(patched_total > 0);
1920 assert_eq!(
1921 apply_reverse_dependency_delta_v0(&mut incremental, final_edges),
1922 0,
1923 "unchanged edge groups must not be patched"
1924 );
1925 }
1926 }
1927
1928 #[test]
1929 fn reverse_dependency_closure_keeps_transitive_source_dependents() {
1930 let edges = vec![
1931 fixture_edge(
1932 "style",
1933 "/workspace/src/Mid.module.scss",
1934 "style",
1935 "/workspace/src/Base.module.scss",
1936 ),
1937 fixture_edge(
1938 "source",
1939 "/workspace/src/App.tsx",
1940 "style",
1941 "/workspace/src/Mid.module.scss",
1942 ),
1943 fixture_edge(
1944 "source",
1945 "/workspace/src/Other.tsx",
1946 "style",
1947 "/workspace/src/Other.module.scss",
1948 ),
1949 ];
1950 let index = reverse_dependency_index_from_edges_v0(edges.as_slice());
1951 let seeds = BTreeSet::from(["/workspace/src/Base.module.scss".to_string()]);
1952 let closure = reverse_dependency_closure_v0(&index, &seeds);
1953
1954 assert!(closure.contains("/workspace/src/Mid.module.scss"));
1955 assert!(closure.contains("/workspace/src/App.tsx"));
1956 assert!(!closure.contains("/workspace/src/Other.tsx"));
1957 }
1958
1959 #[test]
1960 fn typed_vocabulary_keeps_raw_catalog_and_lossy_fold_visible() {
1961 assert_eq!(UNIFIED_HYPERGRAPH_EDGE_KIND_VARIANTS_V0.len(), 11);
1962 assert_eq!(CROSS_FILE_SUMMARY_RAW_EDGE_KIND_VARIANTS_V0.len(), 22);
1963 assert_eq!(CROSS_FILE_SUMMARY_RAW_EDGE_KIND_LABELS_V0.len(), 22);
1964 assert_eq!(
1965 CROSS_FILE_SUMMARY_RAW_EDGE_KIND_VARIANTS_V0
1966 .iter()
1967 .map(|kind| kind.as_wire_label())
1968 .collect::<Vec<_>>(),
1969 CROSS_FILE_SUMMARY_RAW_EDGE_KIND_LABELS_V0
1970 );
1971 assert!(
1972 CROSS_FILE_SUMMARY_RAW_EDGE_KIND_LABELS_V0.contains(&"sourceSelectorPrefixReference")
1973 );
1974
1975 let prefix_kind =
1976 parse_cross_file_summary_raw_edge_kind_v0("sourceSelectorPrefixReference");
1977 assert_eq!(
1978 prefix_kind.map(OmenaCrossFileSummaryRawEdgeKindV0::folded_edge_kind),
1979 Some(UnifiedHypergraphEdgeKindV0::ForeignReference)
1980 );
1981 assert_eq!(
1982 prefix_kind.map(OmenaCrossFileSummaryRawEdgeKindV0::folded_by_lossy_catch_all),
1983 Some(true)
1984 );
1985 assert!(parse_cross_file_summary_raw_edge_kind_v0("strayEdgeKind").is_none());
1986 assert!(parse_cross_file_summary_node_role_v0("runtime").is_none());
1987 }
1988
1989 #[test]
1990 fn raw_edge_catalog_has_total_order_relevance() {
1991 let classified = CROSS_FILE_SUMMARY_RAW_EDGE_KIND_VARIANTS_V0
1992 .iter()
1993 .map(|kind| (kind.as_wire_label(), kind.order_relevance().as_wire_label()))
1994 .collect::<Vec<_>>();
1995
1996 assert_eq!(
1997 classified.len(),
1998 CROSS_FILE_SUMMARY_RAW_EDGE_KIND_LABELS_V0.len()
1999 );
2000 assert!(classified.iter().all(|(kind, relevance)| {
2001 !kind.is_empty() && matches!(*relevance, "orderBearing" | "orderNeutral")
2002 }));
2003 assert_eq!(
2004 OmenaCrossFileSummaryRawEdgeKindV0::CssModulesImport.order_relevance(),
2005 EdgeOrderRelevanceV0::OrderBearing
2006 );
2007 assert_eq!(
2008 OmenaCrossFileSummaryRawEdgeKindV0::SourceSelectorReference.order_relevance(),
2009 EdgeOrderRelevanceV0::OrderNeutral
2010 );
2011 }
2012
2013 #[test]
2014 fn summary_view_recomputes_raw_counts_from_edges() {
2015 let edges = vec![
2016 fixture_edge_with_kind(
2017 "cssModulesComposesImport",
2018 "style",
2019 "/workspace/src/Button.module.scss",
2020 "style",
2021 "/workspace/src/Base.module.scss",
2022 ),
2023 fixture_edge_with_kind(
2024 "sourceSelectorPrefixReference",
2025 "source",
2026 "/workspace/src/Button.tsx",
2027 "style",
2028 "/workspace/src/Button.module.scss",
2029 ),
2030 ];
2031 let summary = summary_from_edges(edges);
2032 let view = summarize_cross_file_summary_view_v0(&summary);
2033 assert!(view.summary_view_ready);
2034 assert_eq!(view.recomputed_edge_kind_counts, summary.edge_kind_counts);
2035
2036 let mut perturbed = summary.clone();
2037 perturbed.edge_kind_counts = vec![OmenaQueryCrossFileSummaryEdgeKindCountV0 {
2038 edge_kind: "cssModulesComposesImport",
2039 count: 99,
2040 }];
2041 let perturbed_view = summarize_cross_file_summary_view_v0(&perturbed);
2042 assert!(!perturbed_view.edge_kind_counts_match_existing_field);
2043 assert_eq!(
2044 perturbed_view.recomputed_edge_kind_counts,
2045 summary.edge_kind_counts
2046 );
2047
2048 let invalid = summary_from_edges(vec![fixture_edge_with_kind(
2049 "strayEdgeKind",
2050 "style",
2051 "/workspace/src/Button.module.scss",
2052 "style",
2053 "/workspace/src/Base.module.scss",
2054 )]);
2055 let invalid_view = summarize_cross_file_summary_view_v0(&invalid);
2056 assert!(!invalid_view.all_raw_edge_kinds_in_catalog);
2057 assert_eq!(invalid_view.invalid_raw_edge_kinds, vec!["strayEdgeKind"]);
2058 }
2059
2060 #[test]
2061 fn graph_delta_records_typed_added_and_removed_edges() {
2062 let stable_edge = fixture_edge_with_kind(
2063 "sourceSelectorReference",
2064 "source",
2065 "/workspace/src/App.tsx",
2066 "style",
2067 "/workspace/src/App.module.scss",
2068 );
2069 let removed_edge = fixture_edge_with_kind(
2070 "cssModulesValueImport",
2071 "style",
2072 "/workspace/src/Tokens.module.scss",
2073 "style",
2074 "/workspace/src/LegacyTokens.module.scss",
2075 );
2076 let added_edge = fixture_edge_with_kind(
2077 "cssModulesComposesImport",
2078 "style",
2079 "/workspace/src/Button.module.scss",
2080 "style",
2081 "/workspace/src/Base.module.scss",
2082 );
2083 let before = summary_from_edges(vec![stable_edge.clone(), removed_edge.clone()]);
2084 let after = summary_from_edges(vec![stable_edge, added_edge.clone()]);
2085 let delta = summarize_cross_file_graph_delta_v0(&before, &after);
2086
2087 assert!(delta.all_delta_edges_typed);
2088 assert_eq!(delta.added_edges.len(), 1);
2089 assert_eq!(delta.removed_edges.len(), 1);
2090 assert_eq!(delta.added_edges[0].edge_id, added_edge.edge_id);
2091 assert_eq!(
2092 delta.added_edges[0].raw_edge_kind,
2093 "cssModulesComposesImport"
2094 );
2095 assert_eq!(
2096 delta.added_edges[0].folded_edge_kind,
2097 UnifiedHypergraphEdgeKindV0::ComposesExternal
2098 );
2099 assert_eq!(delta.removed_edges[0].edge_id, removed_edge.edge_id);
2100 }
2101
2102 fn reverse_dependency_edit_sequences() -> Vec<Vec<Vec<OmenaQueryCrossFileSummaryEdgeV0>>> {
2103 let empty = Vec::new();
2104 let source_a = fixture_edge(
2105 "source",
2106 "/workspace/src/App.tsx",
2107 "style",
2108 "/workspace/src/A.module.scss",
2109 );
2110 let source_b = fixture_edge(
2111 "source",
2112 "/workspace/src/App.tsx",
2113 "style",
2114 "/workspace/src/B.module.scss",
2115 );
2116 let other_b = fixture_edge(
2117 "source",
2118 "/workspace/src/Other.tsx",
2119 "style",
2120 "/workspace/src/B.module.scss",
2121 );
2122 let mid_a = fixture_edge(
2123 "style",
2124 "/workspace/src/Mid.module.scss",
2125 "style",
2126 "/workspace/src/A.module.scss",
2127 );
2128 let source_mid = fixture_edge(
2129 "source",
2130 "/workspace/src/App.tsx",
2131 "style",
2132 "/workspace/src/Mid.module.scss",
2133 );
2134 let leaf_mid = fixture_edge(
2135 "style",
2136 "/workspace/src/Leaf.module.scss",
2137 "style",
2138 "/workspace/src/Mid.module.scss",
2139 );
2140 let source_leaf = fixture_edge(
2141 "source",
2142 "/workspace/src/Deep.tsx",
2143 "style",
2144 "/workspace/src/Leaf.module.scss",
2145 );
2146 let value_a = fixture_edge(
2147 "style",
2148 "/workspace/src/Value.module.scss",
2149 "style",
2150 "/workspace/src/A.module.scss",
2151 );
2152
2153 vec![
2154 vec![
2155 empty.clone(),
2156 vec![source_a.clone()],
2157 vec![source_a.clone(), other_b.clone()],
2158 ],
2159 vec![
2160 vec![source_a.clone()],
2161 vec![source_b.clone()],
2162 vec![source_b.clone(), other_b.clone()],
2163 ],
2164 vec![
2165 vec![source_a.clone(), other_b.clone()],
2166 vec![other_b.clone()],
2167 vec![source_a.clone(), other_b.clone()],
2168 ],
2169 vec![
2170 vec![mid_a.clone()],
2171 vec![mid_a.clone(), source_mid.clone()],
2172 vec![source_mid.clone()],
2173 ],
2174 vec![
2175 vec![mid_a.clone(), source_mid.clone()],
2176 vec![mid_a.clone(), source_mid.clone(), leaf_mid.clone()],
2177 vec![leaf_mid.clone(), source_leaf.clone()],
2178 ],
2179 vec![
2180 vec![source_leaf.clone()],
2181 vec![source_leaf.clone(), value_a.clone()],
2182 vec![value_a.clone()],
2183 ],
2184 vec![
2185 empty.clone(),
2186 vec![source_b.clone(), other_b.clone()],
2187 vec![source_a.clone(), other_b.clone()],
2188 ],
2189 vec![vec![value_a.clone()], empty.clone(), vec![value_a.clone()]],
2190 vec![
2191 vec![leaf_mid.clone()],
2192 vec![leaf_mid.clone(), source_leaf.clone()],
2193 vec![source_leaf.clone()],
2194 ],
2195 vec![
2196 vec![source_mid.clone(), source_leaf.clone()],
2197 vec![source_mid.clone()],
2198 vec![source_mid.clone(), source_leaf.clone()],
2199 ],
2200 vec![
2201 vec![mid_a.clone(), value_a.clone()],
2202 vec![value_a.clone()],
2203 vec![mid_a.clone(), value_a.clone()],
2204 ],
2205 vec![
2206 vec![source_a.clone()],
2207 vec![source_a.clone(), mid_a.clone()],
2208 vec![mid_a, source_mid],
2209 ],
2210 ]
2211 }
2212
2213 fn sequence_has_add_and_remove_for_same_origin(
2214 sequence: &[Vec<OmenaQueryCrossFileSummaryEdgeV0>],
2215 ) -> bool {
2216 sequence.windows(3).any(|window| {
2217 let first = reverse_dependency_groups_by_from_path(window[0].as_slice());
2218 let second = reverse_dependency_groups_by_from_path(window[1].as_slice());
2219 let third = reverse_dependency_groups_by_from_path(window[2].as_slice());
2220 first.keys().any(|from_path| {
2221 let first_targets = first.get(from_path).cloned().unwrap_or_default();
2222 let second_targets = second.get(from_path).cloned().unwrap_or_default();
2223 let third_targets = third.get(from_path).cloned().unwrap_or_default();
2224 first_targets != second_targets
2225 && first_targets == third_targets
2226 && !first_targets.is_empty()
2227 })
2228 })
2229 }
2230
2231 fn fixture_edge(
2232 from_kind: &'static str,
2233 from_path: &str,
2234 target_kind: &'static str,
2235 target_path: &str,
2236 ) -> OmenaQueryCrossFileSummaryEdgeV0 {
2237 fixture_edge_with_kind(
2238 "fixtureDependency",
2239 from_kind,
2240 from_path,
2241 target_kind,
2242 target_path,
2243 )
2244 }
2245
2246 fn fixture_edge_with_kind(
2247 edge_kind: &'static str,
2248 from_kind: &'static str,
2249 from_path: &str,
2250 target_kind: &'static str,
2251 target_path: &str,
2252 ) -> OmenaQueryCrossFileSummaryEdgeV0 {
2253 OmenaQueryCrossFileSummaryEdgeV0 {
2254 edge_id: format!("{from_kind}:{from_path}->{target_kind}:{target_path}"),
2255 edge_kind,
2256 from_kind,
2257 from_path: from_path.to_string(),
2258 target_kind: Some(target_kind),
2259 target_path: Some(target_path.to_string()),
2260 source: None,
2261 owner_selector_name: None,
2262 local_name: None,
2263 remote_name: None,
2264 target_names: Vec::new(),
2265 status: "resolved",
2266 provenance: vec!["omena-query.cross-file-summary.fixture"],
2267 linear_provenance: OmenaCrossFileLinearProvenanceV0::from_static_labels(&[
2268 "omena-query.cross-file-summary.fixture",
2269 ]),
2270 }
2271 }
2272
2273 fn summary_from_edges(
2274 edges: Vec<OmenaQueryCrossFileSummaryEdgeV0>,
2275 ) -> OmenaQueryCrossFileSummaryV0 {
2276 OmenaQueryCrossFileSummaryV0 {
2277 schema_version: "0",
2278 product: "omena-query.cross-file-summary",
2279 status: "fixtureSummary",
2280 summary_scope: "workspaceStyleAndSource",
2281 style_count: 1,
2282 summary_edge_count: edges.len(),
2283 edge_kind_counts: recompute_cross_file_summary_raw_edge_kind_counts_v0(
2284 edges.as_slice(),
2285 ),
2286 summary_hash: stable_omena_query_cross_file_summary_hash(edges.as_slice()),
2287 edges,
2288 capabilities: OmenaQueryCrossFileSummaryCapabilitiesV0 {
2289 css_modules_composes_edges_ready: true,
2290 css_modules_value_edges_ready: true,
2291 css_modules_icss_edges_ready: true,
2292 sass_module_edges_ready: true,
2293 style_design_token_reference_edges_ready: true,
2294 source_selector_reference_edges_ready: true,
2295 stable_summary_hash_ready: true,
2296 linear_provenance_ready: true,
2297 linear_provenance_round_trip_ready: true,
2298 linear_provenance_semiring_laws_hold: true,
2299 },
2300 next_priorities: Vec::new(),
2301 }
2302 }
2303
2304 fn reverse_dependency_index_fingerprint(index: &ReverseDependencyIndexV0) -> String {
2305 let mut parts = Vec::new();
2306 for (target, dependents) in &index.rev {
2307 parts.push(format!(
2308 "rev:{target}={}",
2309 dependents.iter().cloned().collect::<Vec<_>>().join(",")
2310 ));
2311 }
2312 for (from_path, targets) in &index.edges_by_from {
2313 parts.push(format!(
2314 "from:{from_path}={}",
2315 targets.iter().cloned().collect::<Vec<_>>().join(",")
2316 ));
2317 }
2318 parts.join("|")
2319 }
2320}