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
//! Variant equivalence checking.
//!
//! This module provides functionality to determine if two HGVS variants
//! represent the same genomic change, even if expressed differently.
//!
//! # Examples
//!
//! ```
//! use ferro_hgvs::{parse_hgvs, MockProvider};
//! use ferro_hgvs::equivalence::{EquivalenceChecker, EquivalenceLevel};
//!
//! // Create an equivalence checker with test data
//! let provider = MockProvider::with_test_data();
//! let checker = EquivalenceChecker::new(provider);
//!
//! // Check if two variants are equivalent
//! let v1 = parse_hgvs("NM_000088.3:c.10del").unwrap();
//! let v2 = parse_hgvs("NM_000088.3:c.10del").unwrap();
//!
//! let result = checker.check(&v1, &v2).unwrap();
//! assert!(matches!(result.level, EquivalenceLevel::Identical));
//! ```
//!
//! # The relation, stated without circularity
//!
//! `normalize` maps description → description, so it cannot define the
//! equivalence relation it is supposed to respect. The relation is defined by
//! `apply`, which maps description → **sequence**, where equality is byte
//! equality on bases:
//!
//! ```text
//! equivalent(a, b) ≝ same accession
//! ∧ determined(a) = determined(b)
//! ∧ ∀ X ∈ determined(a) : apply_X(a) = apply_X(b)
//! ```
//!
//! An axis is **determined** when the description's coordinates can be carried
//! to it by a mapping that is a function of reference data alone — exon
//! alignments, CDS offsets — and never of normalization. A `c.`/`n.`/`r.`
//! description determines two axes: the transcript, and the genome its exon
//! alignment carries it to. A `g.`/`m.` description determines only the genome.
//! Protein is excluded: translation is many-to-one, and `p.` states a
//! consequence rather than a denotation.
//!
//! # Equivalence Levels
//!
//! The checker recognizes several levels of equivalence:
//!
//! - **Identical**: Same string representation
//! - **NormalizedMatch**: Same after normalization (e.g., different positions in repeat region)
//! - **CrossAxisSequenceMatch**: Same resulting sequence on **every** determined
//! axis. This is the rung that establishes variant identity, and the one a
//! confluence gate requires
//! - **SequenceMatch**: Different normalized strings, same resulting sequence
//! on the description's own axis (e.g. a length-changing `delins` vs a
//! decomposed cis allele of the same edit). True, and *insufficient for
//! identity* — `LRG_199t1:c.3921dup` and `c.3922dup` reach it while denoting
//! genomic changes ~2,790 bp apart, in different exons
//! (`DNA/duplication.md:148`)
//! - **AccessionVersionDifference**: Same variant, different accession versions
//! - **NotEquivalent**: Represent different changes
//! - **Indeterminate**: Neither — at least one side has no computable
//! denotation. Not a negative verdict; see [`EquivalenceLevel::is_decided`]
//!
//! [`EquivalenceLevel::is_at_least`] expresses the order over the denotational
//! rungs, and deliberately makes **NormalizedMatch unusable as a gate**: that
//! rung consults the normalizer, so gating on it would restore the circularity
//! above.
//!
//! # Two entry points, because the short-circuit is not always wanted
//!
//! [`EquivalenceChecker::check`] walks the whole ladder and answers the first
//! rung that fires, so a pair whose normalized forms coincide is reported
//! `NormalizedMatch` and no denotational rung is reached for it. That is the
//! right answer to "are these the same?", and the wrong one to "what did the
//! denotational comparison find?" — two questions that only look alike while
//! the normalizer is non-confluent over the pair in front of you.
//!
//! [`EquivalenceChecker::compare_denotations`] answers the second question. It
//! runs the sequence rung and the cross-axis strengthening over the two
//! descriptions **as written**, normalizing neither, so its answer does not
//! depend on how confluent the normalizer happens to be. Reach for it when the
//! comparison itself is the subject — a confluence gate, or a test of these
//! rungs (#1800).
//!
//! The list is open-ended — [`EquivalenceLevel`] is `#[non_exhaustive]`, so
//! recognizing a new class of equivalence adds a level without breaking
//! downstream matches.
//!
//! # Pairwise levels, or a groupable key
//!
//! [`EquivalenceChecker`] answers a rich question about **two** variants, and
//! its answer is a level rather than a value — `AccessionVersionDifference` is
//! decided by comparing *two* accessions' versions, so there is no scalar a
//! bucket could be keyed on. A consumer that wants to *count distinct changes*
//! across a whole call set needs the other shape: one value per variant, equal
//! exactly when the bases are. That is [`SpdiKey`]. Equal keys cover the
//! `Identical`, `NormalizedMatch` and `SequenceMatch` rungs at once — including
//! the case `SequenceMatch` exists for, where two partitions of one edit reach
//! distinct canonical forms, as the spec's own `DNA/delins.md:44-47` pair does.
//! The key does **not** carry `AccessionVersionDifference`, and it has one
//! documented residual where equal bases key unequally; see [`spdi_key`].
//!
//! Note the implication runs one way. Equal keys imply one of those three rungs,
//! but a pair *at* one of them need not key at all: two identical `p.`
//! descriptions are `Identical` and have no key, because [`spdi_key`] refuses
//! rather than guess. [`spdi_key`] enumerates every such class.
//!
//! Where the two genuinely disagree is the **axis count**, and this module's
//! own counterexample is the witness: [`spdi_key`] buckets `c.3921dup`
//! **with** `c.3922dup` — they are transcript-apply-equal, so their canonical
//! transcript SPDIs coincide — while
//! [`EquivalenceLevel::CrossAxisSequenceMatch`] separates them. A grouping
//! built on the key answers "how many distinct edits are in this call set?";
//! only the cross-axis rung answers "are these the same variant?".
//!
//! # References
//!
//! - [HGVS Nomenclature](https://hgvs-nomenclature.org/)
//! - [Variant Normalization](https://www.ncbi.nlm.nih.gov/variation/notation/)
pub use ;
pub use ;
pub use ;