use super::{
BTreeMap, BTreeSet, CloneClass, Confidence, GroupingConfig, GroupingUnit, OperationKind,
RuleMatch, SOG_SCHEMA_VERSION, SemanticOperationGraph, SemanticRule, SimilarityEdge, grouping,
match_registered_rule, registered_rules,
};
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub struct SemanticCandidateConfig {
pub max_bucket_members: usize,
pub max_candidate_pairs: usize,
}
impl Default for SemanticCandidateConfig {
fn default() -> Self {
Self {
max_bucket_members: 256,
max_candidate_pairs: 16_384,
}
}
}
#[derive(Debug, Clone, Default, PartialEq, Eq)]
pub struct SemanticCandidateStats {
pub graphs: usize,
pub ineligible_graphs: usize,
pub buckets: usize,
pub oversized_buckets: usize,
pub pairs_available: usize,
pub pairs_budget_dropped: usize,
pub pairs_emitted: usize,
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord)]
pub struct SemanticCandidatePair {
pub left: usize,
pub right: usize,
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub struct SemanticGroupingUnit {
pub key: [u8; 16],
}
#[derive(Debug, Clone, Copy, PartialEq)]
pub struct VerifiedSemanticPair {
pub candidate: SemanticCandidatePair,
pub matched: RuleMatch,
}
#[derive(Debug, Clone, PartialEq)]
pub struct SemanticRuleGroup {
pub rule: SemanticRule,
pub canonical: usize,
pub members: Vec<usize>,
pub min_pairwise: f64,
}
#[derive(Debug, Clone, Copy, PartialEq)]
pub struct UngroupedSemanticPair {
pub pair: VerifiedSemanticPair,
pub severed_by_the_ceiling: bool,
}
#[derive(Debug, Clone, Default, PartialEq, Eq)]
pub struct SemanticGroupingStats {
pub verified_pairs: usize,
pub duplicate_pairs: usize,
pub invalid_pairs: usize,
pub grouped_pairs: usize,
pub ungrouped_pairs: usize,
pub ceiling_severed_pairs: usize,
pub groups: usize,
}
#[derive(Debug, Clone, PartialEq)]
pub struct SemanticGrouping {
pub groups: Vec<SemanticRuleGroup>,
pub ungrouped: Vec<UngroupedSemanticPair>,
pub stats: SemanticGroupingStats,
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct SemanticCandidateExtraction {
pub pairs: Vec<SemanticCandidatePair>,
pub stats: SemanticCandidateStats,
}
#[must_use]
pub fn extract_registered_candidates(
graphs: &[SemanticOperationGraph],
config: SemanticCandidateConfig,
) -> SemanticCandidateExtraction {
#[derive(Debug, Clone, PartialEq, Eq, PartialOrd, Ord)]
struct CandidateKey {
variant: [u8; 32],
language: &'static str,
operations: Vec<OperationKind>,
}
let mut stats = SemanticCandidateStats {
graphs: graphs.len(),
..SemanticCandidateStats::default()
};
let mut index: BTreeMap<CandidateKey, Vec<usize>> = BTreeMap::new();
for (index_in_input, graph) in graphs.iter().enumerate() {
if graph.schema_version != SOG_SCHEMA_VERSION
|| !registered_rules()
.iter()
.any(|rule| rule.pattern.accepts(graph))
{
stats.ineligible_graphs += 1;
continue;
}
index
.entry(CandidateKey {
variant: graph.build_variant_fingerprint,
language: graph.language.name(),
operations: graph.nodes.iter().map(|node| node.kind).collect(),
})
.or_default()
.push(index_in_input);
}
stats.buckets = index.len();
let mut pairs = Vec::new();
for members in index.into_values() {
if members.len() > config.max_bucket_members {
stats.oversized_buckets += 1;
continue;
}
let available = members
.len()
.saturating_mul(members.len().saturating_sub(1))
/ 2;
stats.pairs_available = stats.pairs_available.saturating_add(available);
if pairs.len().saturating_add(available) > config.max_candidate_pairs {
stats.pairs_budget_dropped = stats.pairs_budget_dropped.saturating_add(available);
continue;
}
for (offset, &left) in members.iter().enumerate() {
pairs.extend(
members[offset + 1..]
.iter()
.copied()
.map(|right| SemanticCandidatePair { left, right }),
);
}
}
stats.pairs_emitted = pairs.len();
SemanticCandidateExtraction { pairs, stats }
}
#[must_use]
pub fn verify_registered_candidates(
graphs: &[SemanticOperationGraph],
candidates: &[SemanticCandidatePair],
) -> Vec<(SemanticCandidatePair, RuleMatch)> {
candidates
.iter()
.filter_map(|&candidate| {
let (Some(left), Some(right)) =
(graphs.get(candidate.left), graphs.get(candidate.right))
else {
return None;
};
match_registered_rule(left, right).map(|rule_match| (candidate, rule_match))
})
.collect()
}
#[must_use]
#[allow(
clippy::too_many_lines,
reason = "the adapter keeps validation, per-rule partitioning, complete-linkage refinement, and every ungrouped-pair reason in one auditable boundary"
)]
pub fn group_verified_semantic_pairs(
units: &[SemanticGroupingUnit],
verified: &[VerifiedSemanticPair],
config: &GroupingConfig,
) -> SemanticGrouping {
let mut stats = SemanticGroupingStats::default();
let mut partitions: BTreeMap<(&str, u32), SemanticRulePartition> = BTreeMap::new();
for &pair in verified {
let candidate = ordered_semantic_pair(pair.candidate);
if candidate.left == candidate.right
|| candidate.left >= units.len()
|| candidate.right >= units.len()
{
stats.invalid_pairs = stats.invalid_pairs.saturating_add(1);
continue;
}
let key = (pair.matched.rule.id, pair.matched.rule.version);
let partition = partitions
.entry(key)
.or_insert_with(|| SemanticRulePartition::new(pair.matched.rule));
if partition
.pairs
.insert(
(candidate.left, candidate.right),
VerifiedSemanticPair {
candidate,
matched: pair.matched,
},
)
.is_some()
{
stats.duplicate_pairs = stats.duplicate_pairs.saturating_add(1);
}
}
let mut groups = Vec::new();
let mut ungrouped = Vec::new();
for partition in partitions.into_values() {
stats.verified_pairs = stats.verified_pairs.saturating_add(partition.pairs.len());
let mut global_members = BTreeSet::new();
for pair in partition.pairs.values() {
global_members.insert(pair.candidate.left);
global_members.insert(pair.candidate.right);
}
let global_members: Vec<_> = global_members.into_iter().collect();
let local_positions: BTreeMap<_, _> = global_members
.iter()
.copied()
.enumerate()
.map(|(local, global)| (global, local))
.collect();
let grouping_units: Vec<_> = global_members
.iter()
.map(|&global| GroupingUnit {
key: units[global].key,
})
.collect();
let edges: Vec<_> = partition
.pairs
.values()
.map(|pair| SimilarityEdge {
a: local_positions[&pair.candidate.left],
b: local_positions[&pair.candidate.right],
similarity: 1.0,
breakdown: None,
class: CloneClass::RestrictedSemantic,
confidence: Confidence::High,
})
.collect();
let grouped = grouping::group(&grouping_units, &edges, config);
let mut represented = BTreeSet::new();
for group in &grouped.groups {
let members: Vec<_> = group
.members
.iter()
.map(|&local| global_members[local])
.collect();
for (offset, &left) in members.iter().enumerate() {
for &right in &members[offset + 1..] {
represented.insert(ordered_usize_pair(left, right));
}
}
groups.push(SemanticRuleGroup {
rule: partition.rule,
canonical: global_members[group.canonical],
members,
min_pairwise: group.min_pairwise,
});
}
for pair in partition.pairs.into_values() {
let endpoints = (pair.candidate.left, pair.candidate.right);
if represented.contains(&endpoints) {
stats.grouped_pairs = stats.grouped_pairs.saturating_add(1);
continue;
}
let severed_by_the_ceiling = grouped.severed_by_the_ceiling(
local_positions[&pair.candidate.left],
local_positions[&pair.candidate.right],
);
if severed_by_the_ceiling {
stats.ceiling_severed_pairs = stats.ceiling_severed_pairs.saturating_add(1);
}
ungrouped.push(UngroupedSemanticPair {
pair,
severed_by_the_ceiling,
});
}
}
stats.ungrouped_pairs = ungrouped.len();
groups.sort_by(|left, right| {
left.rule
.id
.cmp(right.rule.id)
.then(left.rule.version.cmp(&right.rule.version))
.then(units[left.canonical].key.cmp(&units[right.canonical].key))
.then(left.members.len().cmp(&right.members.len()))
});
ungrouped.sort_by(|left, right| {
left.pair
.matched
.rule
.id
.cmp(right.pair.matched.rule.id)
.then(
left.pair
.matched
.rule
.version
.cmp(&right.pair.matched.rule.version),
)
.then(left.pair.candidate.cmp(&right.pair.candidate))
});
stats.groups = groups.len();
SemanticGrouping {
groups,
ungrouped,
stats,
}
}
struct SemanticRulePartition {
rule: SemanticRule,
pairs: BTreeMap<(usize, usize), VerifiedSemanticPair>,
}
impl SemanticRulePartition {
const fn new(rule: SemanticRule) -> Self {
Self {
rule,
pairs: BTreeMap::new(),
}
}
}
const fn ordered_semantic_pair(pair: SemanticCandidatePair) -> SemanticCandidatePair {
let (left, right) = ordered_usize_pair(pair.left, pair.right);
SemanticCandidatePair { left, right }
}
const fn ordered_usize_pair(left: usize, right: usize) -> (usize, usize) {
if left <= right {
(left, right)
} else {
(right, left)
}
}