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 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
875pub 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(¤t).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
1151const DEFAULT_CYCLE_ENUMERATION_WORK_CAP: usize = 1 << 16;
1154
1155pub 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
1167pub 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 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
1445pub 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}