1use super::{
2 BTreeMap, BTreeSet, CloneClass, Confidence, GroupingConfig, GroupingUnit, OperationKind,
3 RuleMatch, SOG_SCHEMA_VERSION, SemanticOperationGraph, SemanticRule, SimilarityEdge, grouping,
4 match_registered_rule, registered_rules,
5};
6
7#[derive(Debug, Clone, Copy, PartialEq, Eq)]
13pub struct SemanticCandidateConfig {
14 pub max_bucket_members: usize,
16 pub max_candidate_pairs: usize,
18}
19
20impl Default for SemanticCandidateConfig {
21 fn default() -> Self {
22 Self {
23 max_bucket_members: 256,
24 max_candidate_pairs: 16_384,
25 }
26 }
27}
28
29#[derive(Debug, Clone, Default, PartialEq, Eq)]
31pub struct SemanticCandidateStats {
32 pub graphs: usize,
34 pub ineligible_graphs: usize,
36 pub buckets: usize,
38 pub oversized_buckets: usize,
40 pub pairs_available: usize,
42 pub pairs_budget_dropped: usize,
44 pub pairs_emitted: usize,
46}
47
48#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord)]
53pub struct SemanticCandidatePair {
54 pub left: usize,
56 pub right: usize,
58}
59
60#[derive(Debug, Clone, Copy, PartialEq, Eq)]
67pub struct SemanticGroupingUnit {
68 pub key: [u8; 16],
70}
71
72#[derive(Debug, Clone, Copy, PartialEq)]
74pub struct VerifiedSemanticPair {
75 pub candidate: SemanticCandidatePair,
77 pub matched: RuleMatch,
79}
80
81#[derive(Debug, Clone, PartialEq)]
87pub struct SemanticRuleGroup {
88 pub rule: SemanticRule,
90 pub canonical: usize,
92 pub members: Vec<usize>,
94 pub min_pairwise: f64,
98}
99
100#[derive(Debug, Clone, Copy, PartialEq)]
102pub struct UngroupedSemanticPair {
103 pub pair: VerifiedSemanticPair,
105 pub severed_by_the_ceiling: bool,
109}
110
111#[derive(Debug, Clone, Default, PartialEq, Eq)]
113pub struct SemanticGroupingStats {
114 pub verified_pairs: usize,
116 pub duplicate_pairs: usize,
119 pub invalid_pairs: usize,
122 pub grouped_pairs: usize,
124 pub ungrouped_pairs: usize,
126 pub ceiling_severed_pairs: usize,
128 pub groups: usize,
130}
131
132#[derive(Debug, Clone, PartialEq)]
134pub struct SemanticGrouping {
135 pub groups: Vec<SemanticRuleGroup>,
137 pub ungrouped: Vec<UngroupedSemanticPair>,
139 pub stats: SemanticGroupingStats,
141}
142
143#[derive(Debug, Clone, PartialEq, Eq)]
145pub struct SemanticCandidateExtraction {
146 pub pairs: Vec<SemanticCandidatePair>,
148 pub stats: SemanticCandidateStats,
150}
151
152#[must_use]
160pub fn extract_registered_candidates(
161 graphs: &[SemanticOperationGraph],
162 config: SemanticCandidateConfig,
163) -> SemanticCandidateExtraction {
164 #[derive(Debug, Clone, PartialEq, Eq, PartialOrd, Ord)]
165 struct CandidateKey {
166 variant: [u8; 32],
167 language: &'static str,
168 operations: Vec<OperationKind>,
169 }
170
171 let mut stats = SemanticCandidateStats {
172 graphs: graphs.len(),
173 ..SemanticCandidateStats::default()
174 };
175 let mut index: BTreeMap<CandidateKey, Vec<usize>> = BTreeMap::new();
176 for (index_in_input, graph) in graphs.iter().enumerate() {
177 if graph.schema_version != SOG_SCHEMA_VERSION
178 || !registered_rules()
179 .iter()
180 .any(|rule| rule.pattern.accepts(graph))
181 {
182 stats.ineligible_graphs += 1;
183 continue;
184 }
185 index
186 .entry(CandidateKey {
187 variant: graph.build_variant_fingerprint,
188 language: graph.language.name(),
189 operations: graph.nodes.iter().map(|node| node.kind).collect(),
190 })
191 .or_default()
192 .push(index_in_input);
193 }
194 stats.buckets = index.len();
195
196 let mut pairs = Vec::new();
197 for members in index.into_values() {
198 if members.len() > config.max_bucket_members {
199 stats.oversized_buckets += 1;
200 continue;
201 }
202 let available = members
203 .len()
204 .saturating_mul(members.len().saturating_sub(1))
205 / 2;
206 stats.pairs_available = stats.pairs_available.saturating_add(available);
207 if pairs.len().saturating_add(available) > config.max_candidate_pairs {
208 stats.pairs_budget_dropped = stats.pairs_budget_dropped.saturating_add(available);
209 continue;
210 }
211 for (offset, &left) in members.iter().enumerate() {
212 pairs.extend(
213 members[offset + 1..]
214 .iter()
215 .copied()
216 .map(|right| SemanticCandidatePair { left, right }),
217 );
218 }
219 }
220 stats.pairs_emitted = pairs.len();
221 SemanticCandidateExtraction { pairs, stats }
222}
223
224#[must_use]
230pub fn verify_registered_candidates(
231 graphs: &[SemanticOperationGraph],
232 candidates: &[SemanticCandidatePair],
233) -> Vec<(SemanticCandidatePair, RuleMatch)> {
234 candidates
235 .iter()
236 .filter_map(|&candidate| {
237 let (Some(left), Some(right)) =
238 (graphs.get(candidate.left), graphs.get(candidate.right))
239 else {
240 return None;
241 };
242 match_registered_rule(left, right).map(|rule_match| (candidate, rule_match))
243 })
244 .collect()
245}
246
247#[must_use]
262#[allow(
263 clippy::too_many_lines,
264 reason = "the adapter keeps validation, per-rule partitioning, complete-linkage refinement, and every ungrouped-pair reason in one auditable boundary"
265)]
266pub fn group_verified_semantic_pairs(
267 units: &[SemanticGroupingUnit],
268 verified: &[VerifiedSemanticPair],
269 config: &GroupingConfig,
270) -> SemanticGrouping {
271 let mut stats = SemanticGroupingStats::default();
272 let mut partitions: BTreeMap<(&str, u32), SemanticRulePartition> = BTreeMap::new();
273 for &pair in verified {
274 let candidate = ordered_semantic_pair(pair.candidate);
275 if candidate.left == candidate.right
276 || candidate.left >= units.len()
277 || candidate.right >= units.len()
278 {
279 stats.invalid_pairs = stats.invalid_pairs.saturating_add(1);
280 continue;
281 }
282 let key = (pair.matched.rule.id, pair.matched.rule.version);
283 let partition = partitions
284 .entry(key)
285 .or_insert_with(|| SemanticRulePartition::new(pair.matched.rule));
286 if partition
287 .pairs
288 .insert(
289 (candidate.left, candidate.right),
290 VerifiedSemanticPair {
291 candidate,
292 matched: pair.matched,
293 },
294 )
295 .is_some()
296 {
297 stats.duplicate_pairs = stats.duplicate_pairs.saturating_add(1);
298 }
299 }
300
301 let mut groups = Vec::new();
302 let mut ungrouped = Vec::new();
303 for partition in partitions.into_values() {
304 stats.verified_pairs = stats.verified_pairs.saturating_add(partition.pairs.len());
305 let mut global_members = BTreeSet::new();
306 for pair in partition.pairs.values() {
307 global_members.insert(pair.candidate.left);
308 global_members.insert(pair.candidate.right);
309 }
310 let global_members: Vec<_> = global_members.into_iter().collect();
311 let local_positions: BTreeMap<_, _> = global_members
312 .iter()
313 .copied()
314 .enumerate()
315 .map(|(local, global)| (global, local))
316 .collect();
317 let grouping_units: Vec<_> = global_members
318 .iter()
319 .map(|&global| GroupingUnit {
320 key: units[global].key,
321 })
322 .collect();
323 let edges: Vec<_> = partition
324 .pairs
325 .values()
326 .map(|pair| SimilarityEdge {
327 a: local_positions[&pair.candidate.left],
328 b: local_positions[&pair.candidate.right],
329 similarity: 1.0,
330 breakdown: None,
331 class: CloneClass::RestrictedSemantic,
332 confidence: Confidence::High,
333 })
334 .collect();
335 let grouped = grouping::group(&grouping_units, &edges, config);
336 let mut represented = BTreeSet::new();
337 for group in &grouped.groups {
338 let members: Vec<_> = group
339 .members
340 .iter()
341 .map(|&local| global_members[local])
342 .collect();
343 for (offset, &left) in members.iter().enumerate() {
344 for &right in &members[offset + 1..] {
345 represented.insert(ordered_usize_pair(left, right));
346 }
347 }
348 groups.push(SemanticRuleGroup {
349 rule: partition.rule,
350 canonical: global_members[group.canonical],
351 members,
352 min_pairwise: group.min_pairwise,
353 });
354 }
355 for pair in partition.pairs.into_values() {
356 let endpoints = (pair.candidate.left, pair.candidate.right);
357 if represented.contains(&endpoints) {
358 stats.grouped_pairs = stats.grouped_pairs.saturating_add(1);
359 continue;
360 }
361 let severed_by_the_ceiling = grouped.severed_by_the_ceiling(
362 local_positions[&pair.candidate.left],
363 local_positions[&pair.candidate.right],
364 );
365 if severed_by_the_ceiling {
366 stats.ceiling_severed_pairs = stats.ceiling_severed_pairs.saturating_add(1);
367 }
368 ungrouped.push(UngroupedSemanticPair {
369 pair,
370 severed_by_the_ceiling,
371 });
372 }
373 }
374 stats.ungrouped_pairs = ungrouped.len();
375 groups.sort_by(|left, right| {
376 left.rule
377 .id
378 .cmp(right.rule.id)
379 .then(left.rule.version.cmp(&right.rule.version))
380 .then(units[left.canonical].key.cmp(&units[right.canonical].key))
381 .then(left.members.len().cmp(&right.members.len()))
382 });
383 ungrouped.sort_by(|left, right| {
384 left.pair
385 .matched
386 .rule
387 .id
388 .cmp(right.pair.matched.rule.id)
389 .then(
390 left.pair
391 .matched
392 .rule
393 .version
394 .cmp(&right.pair.matched.rule.version),
395 )
396 .then(left.pair.candidate.cmp(&right.pair.candidate))
397 });
398 stats.groups = groups.len();
399 SemanticGrouping {
400 groups,
401 ungrouped,
402 stats,
403 }
404}
405
406struct SemanticRulePartition {
408 rule: SemanticRule,
409 pairs: BTreeMap<(usize, usize), VerifiedSemanticPair>,
410}
411
412impl SemanticRulePartition {
413 const fn new(rule: SemanticRule) -> Self {
414 Self {
415 rule,
416 pairs: BTreeMap::new(),
417 }
418 }
419}
420
421const fn ordered_semantic_pair(pair: SemanticCandidatePair) -> SemanticCandidatePair {
423 let (left, right) = ordered_usize_pair(pair.left, pair.right);
424 SemanticCandidatePair { left, right }
425}
426
427const fn ordered_usize_pair(left: usize, right: usize) -> (usize, usize) {
429 if left <= right {
430 (left, right)
431 } else {
432 (right, left)
433 }
434}