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
//! OSD fallback strategy — Step 3 of the per-candidate decode
//! staircase.
//!
//! Runs only when Steps 1–2 (BP staircase) fail and the caller asks
//! for `BpAllOsd` depth at sufficient `sync_quality`. Computes a
//! fresh f32 LLR bundle for the candidate (OSD operates on f32
//! regardless of the embedded fixed-point `LlrT`), walks the four
//! LLR variants (a/b/c/d) through the WSJT-X-faithful OSD entry
//! ([`osd_decode_npre1`] for low-`q` candidates, [`osd_decode_npre1_npre2`]
//! for `q >= Q_NDEEP3_THRESHOLD`), and applies the
//! `nharderrors > 36` cycle gate to weed out high-error CRC-luck
//! codewords.
//!
//! ε.6 of the `docs/CLEANUP_2026_05.md` `decode_block` split. As of
//! issue **#63** this module hosts the WSJT-X-faithful OSD dispatch
//! — the q-conditional split mirrors `osd174_91.f90`'s ndeep=2/3
//! dispatch table, replacing the previous mfsk-core-specific
//! brute-force ndeep=2/3 split (`osd_decode` / `osd_decode_deep`).
use DecodeDepth;
use crateCmplx;
use crateBpResult;
use crate;
/// `sync_quality` threshold for dispatching to the heavier WSJT-X
/// ndeep=3 entry ([`osd_decode_npre1_npre2`]) instead of ndeep=2
/// ([`osd_decode_npre1`]). Mirrors the pre-#63 dispatch's
/// `q >= 18` split between `osd_decode_deep(_, 3, _)` and
/// `osd_decode(_)` (ndeep=2), now with WSJT-X-faithful internals on
/// both sides.
const Q_NDEEP3_THRESHOLD: u32 = 18;
/// OSD `nharderrors` ceiling — matches WSJT-X's universal
/// `WSJTX_NHARDERRORS_MAX = 36` (`ft8b.f90:422`) exactly, same as the
/// BP variants in `process_one_candidate_inner`.
///
/// **History (issue #72 follow-up, 2026-07-18): was 22, a deliberate
/// mfsk-core-specific deviation, now retracted.** The gate had been
/// tightened to 22 specifically to filter 3 candidates on
/// `qso3_busy.wav` — `N1API F2VX 73` (e=30), `N1API HA6FQ -23` (e=25),
/// `CQ EA2BFM IN83` (e=31) — judged phantoms (CRC-14-luck false
/// accepts) because JTDX's own 18-entry recall list wasn't independently
/// verified for those specific entries (see issue #150). A CCIR-fading
/// sensitivity investigation (`docs/notes/FT8_BENCHMARK.md`) using
/// `ft8sim`-synthesized WAVs with a *known* golden message found the 22
/// ceiling silently discarding genuine golden decodes under heavy
/// fading — traced multiple independent LLR variants converging on the
/// exact transmitted text at `hard_errors` in the high 20s/low 30s.
/// Loosening back to WSJT-X's 36 recovered those, and as a direct
/// side effect also recovered the same 3 `qso3_busy.wav` candidates at
/// their *exact* JTDX-claimed text — independent corroboration between
/// two separate decoders on a CRC-14-protected message is strong
/// evidence against coincidence, resolving them as real rather than
/// phantom. Full regression suite (host `ft8_qso3_apoff_recall`,
/// `ft8_decode_block_real_qso`, embedded `decode_block` paths) showed
/// zero change from this widening — nothing was relying on the
/// tightened gate to stay green.
const OSD_HARDERRORS_MAX: u32 = 36;
/// Pass-ID range emitted by the OSD fallback (`14..=17` mirrors the
/// llr-variant order `a/b/c/d` — see the post-0.6.1 pass-ID layout
/// documented inline below). Exposed so `process_one_candidate_inner`
/// can size its pass-tracking buffers without re-declaring magic
/// numbers.
pub const PASS_ID_OSD_A: u8 = 14;
/// Try the OSD staircase for a candidate whose BP staircase didn't
/// converge. Returns `Some((bp_result, pass_id))` on the first
/// accepted CRC-pass codeword, `None` otherwise.
///
/// Gates internally on `depth = BpAllOsd` and `q >= 12` so the caller
/// can invoke unconditionally — keeps the per-candidate staircase in
/// `process_one_candidate_inner` straightforward (`accepted = accepted
/// .or_else(|| osd_strategy::try_fallback(...))`).
pub