codehelion-core 0.1.0

Engine and intermediate representation for the codehelion source-audit tool.
Documentation
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
use super::{
    BTreeMap, BTreeSet, CloneClass, Confidence, GroupingConfig, GroupingUnit, OperationKind,
    RuleMatch, SOG_SCHEMA_VERSION, SemanticOperationGraph, SemanticRule, SimilarityEdge, grouping,
    match_registered_rule, registered_rules,
};

/// Limits for the registered SOG candidate index.
///
/// Both limits cut whole index buckets. Cutting part of a bucket would make
/// the answer depend on incidental graph order, and could leave a reported
/// group with unexamined peers that look equally eligible.
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub struct SemanticCandidateConfig {
    /// Largest operation-sequence bucket that may enter verification.
    pub max_bucket_members: usize,
    /// Largest number of candidate pairs the extraction may return.
    pub max_candidate_pairs: usize,
}

impl Default for SemanticCandidateConfig {
    fn default() -> Self {
        Self {
            max_bucket_members: 256,
            max_candidate_pairs: 16_384,
        }
    }
}

/// Accounting for registered SOG candidate extraction.
#[derive(Debug, Clone, Default, PartialEq, Eq)]
pub struct SemanticCandidateStats {
    /// Graphs presented to the extractor.
    pub graphs: usize,
    /// Graphs outside the current schema or too short for a registered rule.
    pub ineligible_graphs: usize,
    /// Distinct BuildVariant-and-operation-sequence buckets formed.
    pub buckets: usize,
    /// Buckets omitted in full for exceeding [`SemanticCandidateConfig::max_bucket_members`].
    pub oversized_buckets: usize,
    /// Pairs in eligible buckets before the run-wide ceiling is applied.
    pub pairs_available: usize,
    /// Pairs omitted in full because accepting their bucket would exceed the ceiling.
    pub pairs_budget_dropped: usize,
    /// Candidate pairs returned to a registered rule verifier.
    pub pairs_emitted: usize,
}

/// One pair selected by the bounded SOG candidate index.
///
/// The positions index the caller's graph slice. They are not source anchors
/// and never become stable finding identifiers.
#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord)]
pub struct SemanticCandidatePair {
    /// Position of the first graph in caller order.
    pub left: usize,
    /// Position of the second graph in caller order.
    pub right: usize,
}

/// Position-free identity of one SOG-owning unit supplied to semantic
/// grouping.
///
/// The index in the input slice identifies the unit only for this invocation.
/// `key` is its normalized semantic fragment fingerprint, used solely for
/// deterministic medoid selection and output ordering.
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub struct SemanticGroupingUnit {
    /// Stable normalized semantic fragment identity.
    pub key: [u8; 16],
}

/// One verified semantic candidate paired with the rule that justified it.
#[derive(Debug, Clone, Copy, PartialEq)]
pub struct VerifiedSemanticPair {
    /// Endpoints into the `SemanticGroupingUnit` input slice.
    pub candidate: SemanticCandidatePair,
    /// The closed registered rule that accepted the endpoints.
    pub matched: RuleMatch,
}

/// A cohesive set of SOG-owning units justified by one registered rule.
///
/// Every pair of members was separately accepted by `rule`. In particular,
/// this is not a connected component of pair matches: an absent pair is
/// treated as incompatible by complete-linkage refinement.
#[derive(Debug, Clone, PartialEq)]
pub struct SemanticRuleGroup {
    /// The sole registered rule that explains every internal relation.
    pub rule: SemanticRule,
    /// The deterministic medoid, indexed into the caller's unit slice.
    pub canonical: usize,
    /// Member unit indices, with the canonical unit first.
    pub members: Vec<usize>,
    /// Weakest accepted internal relation. This is always `1.0` for the
    /// binary registered-rule relation, but is retained as explicit evidence
    /// of the complete-linkage contract.
    pub min_pairwise: f64,
}

/// Semantic pairs left outside a cohesive group, with an explicit reason.
#[derive(Debug, Clone, Copy, PartialEq)]
pub struct UngroupedSemanticPair {
    /// The verified pair that no emitted group jointly represents.
    pub pair: VerifiedSemanticPair,
    /// Whether the grouping ceiling prevented this pair from being considered
    /// alongside the other endpoint, rather than complete-linkage rejecting a
    /// non-transitive chain.
    pub severed_by_the_ceiling: bool,
}

/// Accounting for registered semantic grouping.
#[derive(Debug, Clone, Default, PartialEq, Eq)]
pub struct SemanticGroupingStats {
    /// Input pairs whose endpoints were in range and non-identical.
    pub verified_pairs: usize,
    /// Duplicate copies of one rule-and-endpoint relation ignored
    /// deterministically.
    pub duplicate_pairs: usize,
    /// Input pairs rejected because an endpoint was outside the unit slice or
    /// both endpoints named the same unit.
    pub invalid_pairs: usize,
    /// Pairs expressed by an emitted cohesive group.
    pub grouped_pairs: usize,
    /// Verified pairs that no emitted group jointly represents.
    pub ungrouped_pairs: usize,
    /// Ungrouped pairs separated only by the grouping ceiling.
    pub ceiling_severed_pairs: usize,
    /// Cohesive rule groups emitted.
    pub groups: usize,
}

/// Cohesive semantic groups and the verified pairs they do not represent.
#[derive(Debug, Clone, PartialEq)]
pub struct SemanticGrouping {
    /// Groups partitioned by registered rule and refined with complete linkage.
    pub groups: Vec<SemanticRuleGroup>,
    /// Verified pairs retained separately when no group holds both endpoints.
    pub ungrouped: Vec<UngroupedSemanticPair>,
    /// Full grouping accounting, including bounded-refinement effects.
    pub stats: SemanticGroupingStats,
}

/// Candidate pairs and their complete accounting.
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct SemanticCandidateExtraction {
    /// Pairs narrowed by the coarse index, in deterministic order.
    pub pairs: Vec<SemanticCandidatePair>,
    /// What the extractor considered and deliberately omitted.
    pub stats: SemanticCandidateStats,
}

/// Extract bounded candidate pairs for registered SOG rules.
///
/// The inverted index partitions first by the complete `BuildVariant`
/// fingerprint and then by the operation-kind sequence. It therefore never
/// reconnects independent build variants and avoids a project-wide all-pairs
/// comparison. API names and type categories remain evidence for the rule
/// verifier rather than becoming a lossy cross-language index key.
#[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 }
}

/// Verify candidate pairs against the registered rules.
///
/// A pair outside the provided slice is ignored rather than guessed at. The
/// extractor only produces in-range pairs, but this makes callers that load
/// persisted candidate data fail closed as well.
#[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()
}

/// Group verified registered-rule pairs without treating pair compatibility as
/// transitive.
///
/// Rules are grouped independently, so a unit cannot connect two different
/// semantic claims merely because it participates in both. Within a rule, a
/// verified pair is a binary relation with similarity `1.0`; any pair the
/// verifier did not accept is absent and therefore reads as incompatible to
/// complete-linkage refinement. This turns a partially connected match graph
/// into cohesive groups while retaining every accepted relation no group can
/// express as an [`UngroupedSemanticPair`].
///
/// Invalid and duplicate inputs are ignored with explicit accounting. The
/// normal verifier cannot create either, but this keeps persisted or adapter
/// supplied pair data fail-closed.
#[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,
    }
}

/// Partition of verified pairs justified by exactly one registered rule.
struct SemanticRulePartition {
    rule: SemanticRule,
    pairs: BTreeMap<(usize, usize), VerifiedSemanticPair>,
}

impl SemanticRulePartition {
    const fn new(rule: SemanticRule) -> Self {
        Self {
            rule,
            pairs: BTreeMap::new(),
        }
    }
}

/// Normalize a semantic pair so duplicate relations have one representation.
const fn ordered_semantic_pair(pair: SemanticCandidatePair) -> SemanticCandidatePair {
    let (left, right) = ordered_usize_pair(pair.left, pair.right);
    SemanticCandidatePair { left, right }
}

/// Normalize one undirected endpoint pair.
const fn ordered_usize_pair(left: usize, right: usize) -> (usize, usize) {
    if left <= right {
        (left, right)
    } else {
        (right, left)
    }
}