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
//! `docs/recommendations/checklist.md:20` — an intronic position stated on a
//! **bare** transcript reference, with no genomic reference to annotate it on.
//!
//! # The clause
//!
//! > an `NM_` reference sequence can only be used to describe variants in
//! > introns using a `c.` prefix when a genomic reference sequence is given on
//! > which the coding DNA reference sequence is annotated
//!
//! The reason is definitional rather than stylistic: a coding or non-coding DNA
//! reference sequence *is* the spliced transcript, so it contains no introns for
//! an offset to name. `NM_000088.3:c.589-1G>T` states a position one nucleotide
//! 5' of an exon boundary on a sequence that has no such nucleotide. The
//! wrapper forms the clause asks for — `NG_(NM_)`, `NC_(NM_)` — supply the
//! genomic sequence the offset is measured on, which is why they are accepted
//! here and the bare form is not.
//!
//! # This is a CONDITIONAL clause, and the mode split follows from that
//!
//! `rulings[bare-transcript-intronic-position]` decided this, and nothing in
//! this module reopens it. Two things make the clause conditional in form
//! rather than an absolute prohibition on a shape:
//!
//! - It says "can only be used … **when**", which states a condition on the
//! reference sequence rather than banning a spelling.
//! - The spec reads a bare-`c.` intronic description itself, four items later:
//! `checklist.md:45` glosses `c.12-14del` as "a deletion of nucleotide -14 in
//! the intron directly 5' of nucleotide `c.12`", with no genomic wrapper in
//! sight. A clause the spec does not apply to its own worked gloss cannot be
//! read as an absolute bar.
//!
//! So strict input hygiene refuses the bare form and lenient accepts it. That
//! is the ruling; the corpus classifies these rows `Strength::Conditional` for
//! the same reason.
//!
//! # Enforcement stage
//!
//! Per the decided `rulings[absolute-prohibition-enforcement-stage]`, the stage
//! is **mode-dependent** and the check belongs at **parse**:
//!
//! - **strict** refuses at parse, because strict validates input conformance
//! rather than merely parseability
//! ([`apply_bare_transcript_intron_rule`](crate::hgvs::parser::parse_hgvs_with_config));
//! - **lenient** accepts at parse with a `W4007` warning, and **silent**
//! accepts without one.
//!
//! That record's census named this clause's stage as the third of the three
//! things it had to fix — "`checklist.md:20`'s refusal is at normalize, not at
//! parse … moving the stage is cosmetic there, but it should move for
//! uniformity" — and #1630 is where it moved. The ground it gives is the
//! general one: "whether the INPUT conforms is answered before the input is
//! accepted, not part-way through normalizing it."
//!
//! **The normalize-stage rung is kept, and is not redundant.** It answers for a
//! caller who reaches the normalizer through some *other* door — the
//! config-less `parse_hgvs`, or a lenient parse followed by a strict
//! `Normalizer` — and it is the rung that carries the `EINTRONIC` tag the
//! Mutalyzer conformance map keys off. The two rungs answer for two different
//! callers and neither subsumes the other.
//!
//! # There is no repair arm
//!
//! Re-expressing a bare `NM_…:c.20+2del` on a genomic reference needs a
//! genomic accession and an exon table, neither of which the parser holds — and
//! choosing *which* genomic parent is a question with more than one answer for
//! any transcript with several placements. `rulings[bare-transcript-intronic-
//! position]` also settles the direction: "An input that already names one is
//! still left as authored", so a repair here would re-spell a description the
//! ruling says to leave alone. Ferro does re-parent an intronic offset it
//! *manufactured* itself (#1704), which is a different question decided on
//! provenance the normalizer has and the parser does not.
use crateUncertainBoundary;
use crate;
use crateHgvsVariant;
/// A bare-transcript intronic position found in a description.
/// Find a bare-transcript intronic position anywhere in `variant`, walking
/// allele members.
///
/// This is the **description-wide** entry, used by the parse-stage rule: a
/// hygiene check that runs once per description rather than once per member
/// stops firing exactly on an allele, which is why the walk is here rather than
/// left to the caller. It delegates to [`bare_transcript_intronic_leaf`] so the
/// clause has one reading; see that function's note on why the split matters.
/// The **per-leaf** predicate: the coordinate system (`"c"` or `"n"`) whose bare
/// transcript reference `variant` names an intronic position on, or `None`.
///
/// In scope: a bare coding transcript (`NM_`/`XM_` or LRG `LRG_<N>t<k>`) used
/// with `c.`, and a bare non-coding transcript (`NR_`/`XR_` via
/// `is_noncoding_rna()`, or LRG) used with `n.`, in both cases with
/// `Accession.genomic_context == None`. The spec's "a (non-)coding DNA
/// reference sequence does not contain introns" rule applies equally to curated
/// (`NM_`/`NR_`), predicted (`XM_`/`XR_`) and LRG transcript references — an LRG
/// transcript is itself a bare reference with no `NG_`/`NC_` genomic context —
/// so all are covered on each axis (#834).
///
/// Out of scope: genomic-context forms (`genomic_context: Some`), `NG_`/`NC_`
/// references (which never reach the `c.`/`n.` transcript path), Ensembl `ENST`,
/// and the `r.` axis.
///
/// Both `Single` and `Range` (uncertain-breakpoint) position boundaries are
/// inspected, so `c.(100+1_101-1)_(200+1_201-1)del` is covered; an unknown (`?`)
/// offset still counts as intronic (`CdsPos::is_intronic` treats the
/// unknown-offset sentinel as intronic), which is correct — it is an intronic
/// position whose exact offset is unspecified.
///
/// # One rule, three callers, and the split is deliberate
///
/// The scope prose above documents this function as much as its callers, and
/// that is load-bearing in three directions at once: the same question decides
/// whether **strict parse refuses an input** (#1630), whether **strict normalize
/// refuses one** (#486), and whether **#1704 must re-parent an output**. Two
/// readings of one clause is how ferro came to refuse a description in strict
/// mode while manufacturing the identical description in lenient. So every
/// caller derives from this predicate rather than restating it, which is what
/// makes them unable to drift.
///
/// The leaf/description split exists because the normalizer wants the leaf form
/// — `normalize_allele` already recurses per member, so a walking predicate
/// would ask the same question twice — while the parser holds the whole
/// description and has no other pass that would reach the members.
/// The bare axis label, for callers that want the answer without building a
/// [`BareTranscriptIntron`].
///
/// The reason this exists separately is cost, not taste. Every `normalize()`
/// asks the question at least twice — once on the strict ladder and once at
/// `Normalizer::reparent_junction_exit` — on a path that wants a `bool`.
/// True iff any concrete position reachable from `boundary` is intronic —
/// covering both a `Single` position and either endpoint of a `Range` boundary
/// (uncertain breakpoints like `c.(100+1_101-1)_(200+1_201-1)del`). `Unknown`
/// (`?`) and otherwise-absent inner positions contribute `false`.