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
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
470
471
472
473
474
475
476
477
478
479
480
481
482
483
484
485
486
487
488
489
490
491
492
493
494
495
496
497
498
499
500
501
502
503
504
505
506
507
508
509
510
511
512
513
514
515
516
517
518
519
520
521
522
523
524
525
526
527
528
529
530
531
532
533
534
535
536
537
538
539
540
541
542
543
544
545
546
547
548
549
550
551
552
553
554
555
556
557
558
559
560
561
562
563
564
565
566
567
568
569
570
571
572
573
574
575
576
577
578
579
580
581
582
583
584
585
586
587
588
589
590
591
592
593
594
595
596
597
598
599
600
601
602
603
604
605
606
607
608
609
610
611
612
613
614
615
616
617
618
619
620
621
622
623
624
625
626
627
628
629
630
631
632
633
634
635
636
637
638
639
640
641
642
643
644
645
646
647
648
649
650
651
652
653
654
655
656
657
658
659
660
661
662
663
664
665
666
667
668
669
670
671
672
673
674
675
676
677
678
679
680
681
682
683
684
685
686
687
688
689
690
691
692
693
694
695
696
697
698
699
700
701
702
703
704
705
706
707
708
709
710
711
712
713
714
715
716
717
718
719
720
721
722
723
724
725
726
727
728
729
730
731
732
733
734
735
736
737
738
739
740
741
742
743
744
745
746
747
748
749
750
751
752
753
754
755
756
757
758
759
760
761
762
763
764
765
766
767
768
769
770
771
772
773
774
775
776
777
778
779
780
781
782
783
784
785
786
787
788
789
790
791
792
793
794
795
796
797
798
799
800
801
802
803
804
805
806
807
808
809
810
811
812
813
814
815
816
817
818
819
820
821
822
823
824
825
826
827
828
829
830
831
832
833
834
835
836
837
838
839
840
841
842
843
844
845
846
847
848
849
850
851
852
853
854
855
856
857
858
859
860
861
862
863
864
865
866
867
868
869
870
871
872
873
874
875
876
877
878
879
880
881
882
883
884
885
886
887
888
889
890
891
892
893
894
895
896
897
898
899
900
901
902
903
904
905
906
907
908
909
910
911
912
913
914
915
//! ADWIN — the honest-blind batch drift-detector (ADR-065), the **loud-class** half
//! of CURATE's automatic decay-trigger.
//!
//! # What this organ is
//!
//! antigen needs to decide WHEN a learned class has gone obsolete (or is being
//! evaded) by watching its per-class affinity-trajectory for a downward change-point.
//! ADWIN (Bifet & Gavaldà 2007, the field-standard streaming concept-drift detector)
//! is the canonical mechanism. This module is antigen's **batch-pure** build of it —
//! NOT the crate's streaming `&mut self` struct (a forbidden second state-store that
//! desyncs from the append-only life-record; ADR-064). The detector is a PURE
//! derivation over the already-materialized
//! [`score_trajectory`](crate::learn::life_record::LifeRecord::score_trajectory).
//!
//! # `UnderPowered` is the SPINE (the Aristotelian move — ADR-065 T1)
//!
//! A change on a stream is detectable only above a statistical-power threshold;
//! below it, detection is mathematically impossible. At antigen's CURRENT scale
//! (classes have matured n≈4-8 times) the bound is DEAD: `2·ε_cut > 1.0` = the max
//! observable signal, so a *correct* detector CANNOT fire. Therefore
//! [`DriftVerdict::UnderPowered`] is not a corner case — it is the DEFAULT verdict
//! for every class today. The organ's entire v0.6 value is that it HONESTLY says
//! "I cannot yet see drift for this class, and here is exactly when I will be able to
//! (`n_star`, computed from the bound, no real data needed)." A detector that fires
//! zero and says-so is the correct, valuable v0.6 organ — and it is the SAME organ
//! that fires correctly once trajectories lengthen, with NO code change.
//!
//! **INV-ADWIN-1: `UnderPowered` is never suppressed.** No wildcard arm in
//! [`DriftVerdict`] processing may collapse it into `NoDrift`. Silence has two causes
//! — no-drift vs can't-see — and they are DISTINCT verdicts (a bare `bool` collapsing
//! them is the silent-miscalibration antigen exists to catch).
//!
//! # The floor→full regime-switch (ADR-065 T1+T5+A6)
//!
//! It is ONE [`DriftVerdict`] type, two regimes that [`detect`] dispatches between:
//!
//! - the **FLOOR** (rigorous ADWIN0, [`eps_cut_floor`]) — `δ'=δ/n`, all-n splits,
//! Theorem-3.1-rigorous, returns `UnderPowered` while blind. Governs below the
//! sample-count the normal-approximation needs.
//! - the **FULL** (variance-aware ADWIN2, [`eps_cut_full`] / [`ExpHistogram`]) — Eq
//! 3.1, `δ'=δ/ln n`, the paper's O(log n) bucket-cut form, normal-approximation.
//! Governs once a class has accumulated enough maturations. NOTE (ADR-065 Amd 2):
//! the shipped [`detect`] scans all `n−1` cuts via the `best_split` helper; the
//! [`ExpHistogram`] O(log n) cutpoint structure is built + self-tested but not yet on
//! the detection path, and the floor governs at v0.6 scale (gated behind `n ≥ 30`).
//! See the [`eps_cut_full`] docstring + ADR-065 Amendment 2.
//!
//! The floor's `UnderPowered` verdict already carries `eps_cut` and `max_observable`;
//! the moment `eps_cut < max_observable` persistently, the class has crossed its
//! power threshold `n*` — the SAME signal the seam reads. The floor's power-guard IS
//! the seam trigger; no separate length-counter.
//!
//! **INV-ADWIN-2: the floor and full `δ'` are NOT interchangeable.** `δ/n` in the
//! full detector over-corrects (loses the sensitivity the EH structure buys); `δ/ln n`
//! in the floor under-corrects (a forgetting-storm). Each regime uses its own; a
//! born-red test asserts it.
//!
//! # The source-verified math (ADR-065 — VERBATIM, do NOT reconstruct from memory)
//!
//! Transcribed verbatim from the Bifet-Gavaldà 2007 PDF (§3.1/3.2 ADWIN0 rigorous
//! form Theorem 3.1, Eq. 3.1 variance-aware ADWIN2, §3.3 exponential-histogram
//! bucket-merge). The constants are load-bearing — getting one wrong is a silent
//! miscalibration. See [`eps_cut_floor`] / [`eps_cut_full`] / [`ExpHistogram`] for
//! the per-formula citations.
use presents;
use crateAffinity;
use crateSilentStatus;
/// The confidence parameter `δ` the synthetic-fixture suite uses (ADR-065).
///
/// Lower = more conservative (fires less). The detector takes `δ` as an argument so
/// callers can tighten it; this is the default the born-red fixtures pin.
pub const DEFAULT_DELTA: f64 = 0.05;
/// The exponential-histogram bucket-count parameter `M` (Bifet-Gavaldà §3.3).
///
/// "The paper's validated default." Keep ≤ `M` buckets of each size `2^i`; on the
/// `M+1`-th, merge the two oldest. The paper's worked trace uses `M=2`; the shipped
/// default is `M=5`.
pub const M_BUCKETS: usize = 5;
/// Which affinity axis a drift-verdict concerns.
///
/// The detector runs PER-AXIS and ORs the alarms (ADR-065): a scalarization (F1 /
/// mean) would hide a drift where one axis craters while the other compensates — the
/// exact interior-crater blindness.
/// WHICH axis drifted is decision-relevant: recall-drop routes to the red-queen
/// (evasion), precision-drop to the autoimmunity effector (over-broad binding).
/// The outcome of a change-point test over an affinity-trajectory (ADR-065's sealed
/// verdict — the spine).
///
/// `UnderPowered` is the default at antigen's scale. Silence has two causes —
/// no-drift vs can't-see — and they are DISTINCT verdicts (INV-ADWIN-1). A bare
/// `bool` collapsing them is the silent-miscalibration antigen exists to catch.
// ============================================================================
// The verified bounds (Bifet-Gavaldà 2007 — VERBATIM, the load-bearing constants)
// ============================================================================
/// Harmonic mean of the two window sizes — `m = 1/(1/n0 + 1/n1)` (Bifet-Gavaldà §3.2).
///
/// Algebraically identical to `n0·n1/(n0+n1)`. Returns `None` for an empty sub-window
/// (no split to test).
// window sizes are small (≤ trajectory length)
/// **FLOOR — the rigorous ADWIN0 `ε_cut` (Bifet-Gavaldà §3.1/3.2, Theorem 3.1).**
///
/// `ε_cut = sqrt( (1/(2m)) · ln(4/δ') )`, with `δ' = δ/n` (the Bonferroni-style
/// correction over the O(n) split-points). **The constant inside `ln` is 4** (NOT 2 —
/// that is the full form's constant; INV-ADWIN-2). The guaranteed-DETECTABLE shift is
/// `2·ε_cut` (Theorem 3.1.2) — this factor-2 is why n≈8 is dead.
///
/// This is the FULLY-rigorous, distribution-free, verifiable bound (Theorem 3.1 holds
/// unconditionally) — the correct floor where antigen lacks the ~30 samples the
/// variance form's normal-approximation needs.
// n is a trajectory length, well within f64
/// **FULL — the variance-aware ADWIN2 `ε_cut` (Bifet-Gavaldà Eq 3.1).**
///
/// `ε_cut = sqrt( (2/m)·σ²_W·ln(2/δ') ) + (2/(3m))·ln(2/δ')`, with `δ' = δ/ln(n)`
/// (the paper's ADWIN2 correction for ~O(log n) bucket-boundary cutpoints; INV-ADWIN-2).
/// **The constant inside `ln` is 2** (NOT 4 — do not copy the floor's). The additive Bernstein term
/// `(2/(3m))·ln(2/δ')` is **NOT optional** — it protects small windows (the normal
/// approximation fails there) and dropping it under-fires in exactly antigen's regime.
///
/// `sigma_sq_w` = the observed sample variance of the per-axis scalar values in W.
///
/// **Rigor caveat (ADR-065):** this is the NORMAL-APPROXIMATION form — "perfectly
/// valid in practice" but "not 100% rigorous," valid only above the sample-count the
/// CLT needs (~30, partially relaxed by the Bernstein term). Below that the floor's
/// rigorous bound governs.
///
/// **FINDING (ADR-065 Amendment 2) — shipped-scan scope.** The `δ' = δ/ln(n)`
/// correction is the union bound for ~O(log n) cutpoints, but the shipped [`detect`]
/// path scans ALL `n−1` interior cuts (the `best_split` helper — "Scan all O(n) interior
/// splits") — the [`ExpHistogram`] O(log n) cutpoint structure is built and self-tested
/// but is NOT on the detection path. This is latent, not live: the full leg is gated
/// behind `n ≥ NORMAL_APPROX_MIN` (= 30) in the `combined_eps_cut` helper, and at v0.6
/// scale every class is `UnderPowered` at n≈4–8, so the rigorous all-`n` floor (`δ/n`)
/// governs every reachable case. The all-cut-vs-O(log n) reconciliation (restrict the
/// scan to the EH boundaries, OR re-derive the full bound's `δ'` for the all-cut scan)
/// is flagged for a build pass in ADR-065 Amendment 2.
// n is a trajectory length, well within f64
/// The maximum observable `|μ_before − μ_after|` for an affinity axis.
///
/// Both recall and precision are rates in `[0,1]`, so the mean-difference is bounded by
/// `1.0`. This is the signal `ε_cut` is compared against for the power-guard.
pub const MAX_OBSERVABLE: f64 = 1.0;
/// Sample mean of a slice (0.0 for an empty slice — only called on non-empty windows).
// slice len is a trajectory length
/// Observed sample variance of a slice (population variance, the paper's `σ²_W`).
// slice len is a trajectory length
/// `n*` — how many points the trajectory needs before this axis becomes drift-observable.
///
/// The smallest `n` whose balanced split (`n0=n1=n/2`, the most-powerful split)
/// satisfies `2·ε_cut_floor ≤ MAX_OBSERVABLE`. Computed from the rigorous floor bound,
/// no real data needed (ADR-065's "self-announce power at n*").
///
/// Returns the **absolute** length (not the remaining count); the caller subtracts the
/// current length for "N more maturations". Bounded search (the bound is monotone
/// decreasing in n, so the first crossing is the answer); capped to avoid a runaway.
// ============================================================================
// The floor detector (rigorous ADWIN0 over a single batch window) + power-guard
// ============================================================================
/// Run the FLOOR detector (rigorous ADWIN0) over one axis's scalar stream. Scans all
/// O(n) splits; fires on the first (the oldest cut, the most history to discard) whose
/// `|μ_W0 − μ_W1| ≥ ε_cut`. Returns the per-axis verdict — including the
/// `UnderPowered` power-guard when the bound exceeds the max observable signal.
///
/// The power-guard (INV-ADWIN-1): the detector is structurally blind iff the
/// GUARANTEED-DETECTABLE shift exceeds the max observable signal. Theorem 3.1.2 sets
/// the guaranteed-detectable shift at `2·ε_cut` (the balanced split is the tightest),
/// so the reported `eps_cut` field is that detectable shift `2·ε_cut_balanced` — the
/// "minimum signal the detector can promise to catch." Blind ⟺ that value
/// `≥ MAX_OBSERVABLE`. Returns [`DriftVerdict::UnderPowered`] then (NOT a silent
/// `NoDrift`); `n*` is available on demand via [`power_threshold_n`].
/// A split's evaluation: its cut index, the observed mean-difference, the bound it was
/// tested against, and the evidence `observed_diff − eps_cut` (positive iff it fires).
/// Scan all O(n) interior splits with `bound`, returning the split with the strongest
/// evidence (and the tightest margin among non-firing splits, surfaced via the same
/// `SplitEval` when nothing fires). `None` only if no split is computable (n < 2).
/// Turn the best split into a verdict: `Drift` if it cleared its bound (evidence ≥ 0),
/// else `NoDrift` carrying how close the closest split came (`tightest_margin`).
/// Floor-bound adapter with the `(n0, n1, n, delta)` signature [`best_split`] expects.
// ============================================================================
// The exponential-histogram (ADWIN2 §3.3) — the FULL regime's window structure
// ============================================================================
/// One exponential-histogram bucket (Bifet-Gavaldà §3.3).
///
/// A power-of-2 number of elements (`capacity = 2^i`) and their summed content. Buckets
/// are kept newest-first within the histogram; a merge combines the two OLDEST of a
/// given size.
/// The exponential-histogram window (ADWIN2 §3.3).
///
/// Buckets are stored **newest-first** (index 0 = the most recent element). Inserting
/// cascades merges of the two OLDEST buckets of each over-full size upward — the
/// structure that buys O(log n) memory and the O(log n) cutpoint set the full bound uses.
///
/// The bucket-merge is the GOLDEN-FIXTURE-tested core (ADR-065): the paper's own
/// worked trace (`Content 4,2,2,1,1` + new `1` → `4,2,2,2,1` → `4,4,2,1` at M=2) is a
/// born-red test — a wrong merge (newest-not-oldest, no-cascade, capacity-but-not-
/// content) fails it.
// ============================================================================
// The public detector — per-axis OR, floor→full regime-switch
// ============================================================================
/// **The drift detector** (ADR-065) — a batch-pure change-point test over a trajectory.
///
/// `delta` (`δ`) is the CONFIDENCE — the false-positive bound (Theorem 3.1); the
/// caller's contract is `δ ∈ (0, 1)` (use [`DEFAULT_DELTA`]). An out-of-range `δ` is
/// CLAMPED into the safe interval rather than allowed to silently miscalibrate the FP
/// guarantee (`δ ≤ 0` ⇒ a NaN/∞ bound; `δ ≥ 1` ⇒ an over-firing bound — the dangerous
/// direction for a decay-trigger). In-range `δ` is untouched.
///
/// Runs PER-AXIS (recall, precision) with `δ_axis = δ/2`
/// (Bonferroni over the two axes) and ORs the alarms — returning the FIRST axis that
/// fires (recall checked first, the red-queen signal never masked). If no axis fires
/// but ANY axis is under-powered, the OR returns `UnderPowered` (INV-ADWIN-1: a blind
/// axis is never collapsed into a confident `NoDrift`).
///
/// # Floor→full regime-switch
///
/// Per axis, [`detect`] dispatches by the trajectory length against the axis's power
/// threshold `n*` ([`power_threshold_n`]): below `n*` the rigorous FLOOR governs (and
/// returns `UnderPowered`); at/above `n*` the variance-aware FULL bound governs
/// (sharper, normal-approximation, valid because n is now large enough). The CALLER
/// sees one function; only the regime-switch lives inside.
///
/// # The herd-drift hook (ADR-065 do-now)
///
/// `detect` reads `&[Affinity]`, which carries no commit-identity, so the returned
/// `Drift` verdict carries no commit-sha (the fusion-contract seals the variant). The
/// herd-drift hook (record the change-point's commit-sha so the future cross-class
/// correlator has a shared time-axis) is closed at the CALLER: when the trajectory was
/// assembled from a [`LifeRecord`]'s `Scored` events, the caller maps `cut_index` → the
/// originating event's commit (once events carry commit-identity). The pure detector
/// stays sha-free (no time-axis threaded through the math).
///
/// [`LifeRecord`]: crate::learn::life_record::LifeRecord
//
// Dogfood (INV-ADWIN-1): this fn IS the failure-locus for
// [`SilentIntentNullification`](crate::stdlib::dogfood::SilentIntentNullification) —
// the one place a blind axis (`UnderPowered`) could be silently collapsed into a
// confident `NoDrift`, or an out-of-range `δ` could silently miscalibrate the FP
// guarantee. It `#[presents]` that class so `cargo antigen audit` sees the defense.
// The born-red `atk_adwin_underpowered_never_suppressed_at_antigen_scale` declares it
// defends the class via `#[defended_by]`; the audit cross-references the two.
/// One axis's detector with the floor→full regime-switch AND recursive change-point
/// descent (so a SINGLE [`detect`] call surfaces an INTERIOR crater).
///
/// # Why recursion (the interior-crater payoff)
///
/// A symmetric interior crater (`0.9→0.2→0.9`) has its STRONGEST single split at a
/// crater boundary, but that split's mean-difference is DILUTED (the post-boundary
/// window still holds the other half of the crater), so a single best-split read can
/// fall below the bound even though a real change-point exists. The standard batch
/// change-point read (the batch analogue of streaming ADWIN's drop-tail-and-re-test) is
/// RECURSIVE: find the best candidate split; if it clears, fire there; otherwise recurse
/// into BOTH sub-windows — a crater's left half (`0.9…0.2`) and right half (`0.2…0.9`)
/// each contain a clearing edge the diluted full-window split missed.
///
/// # The honest power-guard (INV-ADWIN-1, preserved)
///
/// The recursion fires only on a split that genuinely clears its δ-bounded `ε_cut`
/// (the FP guarantee holds). When NO sub-window anywhere has a clearing split, the
/// verdict is `UnderPowered` iff the window is structurally blind — its most-powerful
/// (balanced) split's guaranteed-detectable shift `2·ε_cut ≥ max_observable`, so no
/// split could EVER clear the max signal (the n≈8 dead-zone) — else a confident
/// `NoDrift`. A short trajectory (n≈8) is blind and SAYS SO; a long stationary one is
/// `NoDrift`; a long crater FIRES via the recursion.
/// Recursively search for a clearing change-point. Returns the first `Drift` found
/// (strongest split in the deepest clearing sub-window), or `None` if no sub-window
/// anywhere clears. Uses the COMBINED bound (the tighter of the rigorous floor and the
/// variance-aware full, see [`combined_eps_cut`]) so a window fires whenever EITHER
/// valid bound is cleared.
/// The COMBINED `ε_cut`: the tighter (smaller) of the rigorous floor bound and — once
/// the window is long enough for the normal approximation (`n ≥ NORMAL_APPROX_MIN`) —
/// the variance-aware full bound. Both are valid δ-bounded upper bounds on the
/// under-H0 deviation, so firing on the tighter one preserves the false-positive
/// guarantee while gaining sensitivity: the floor is tighter on a high-variance
/// balanced split (a symmetric crater edge), the full is tighter on a low-variance
/// stream — taking the min uses whichever bound the data makes sharp.
/// The sample-count the variance-aware normal approximation needs (the paper's ~30,
/// partially relaxed by the Bernstein term). Below it the rigorous floor governs.
const NORMAL_APPROX_MIN: usize = 30;
/// The full-window tightest margin (smallest `ε_cut − observed_diff` over splits) — the
/// `NoDrift` payload when no split clears.
// ============================================================================
// The two-channel fusion (INV-ADWIN-3 — the conservatism-JOIN into ClassVerdict)
// ============================================================================
/// **Fuse the two afferent channels into a curation verdict (INV-ADWIN-3 — the
/// conservatism-JOIN).** The producer of [`ClassVerdict`] for the LOUD classes.
///
/// Joins the ADWIN temporal channel (`adwin`) with the bit-3 static-shape channel
/// (`silent` + `defended`, the same two inputs the streamless
/// [`classify`](crate::learn::discriminator::classify) reads). Returns the
/// [`ClassVerdict`] the efferent loops (CURATE) act on — so this is what *produces*
/// [`ClassVerdict::Obsolete`], the one auto-forgettable cell. A fusion bug that emits
/// `Obsolete` when a channel is blind bypasses CURATE's moral-center gate entirely (the
/// gate holds, but the wrong key is handed to it). Hence the hard constraint:
///
/// **THE CONSERVATISM-JOIN (the safety floor, ADR-065 Phase 6 C2):** if
/// EITHER channel is blind — ADWIN [`DriftVerdict::UnderPowered`] OR bit-3
/// [`SilentStatus::Indeterminate`] — the verdict is [`ClassVerdict::RouteToHuman`]
/// (HOLD, never auto-forget), regardless of what the other channel says. A blind
/// channel cannot endorse an irreversible forget.
///
/// The fusion table (ADR-065 §real/virtual fusion), once BOTH channels are sighted:
///
/// | ADWIN signal | bit-3 (`silent`/`defended`) ⇒ verdict |
/// |-------------------------|----------------------------------------|
/// | `Drift` (recall-drop) | drives the bit-3 split: shape-gone-undefended ⇒ `Obsolete`; shape-gone-defended ⇒ `WellDefended`; `Evading` ⇒ `Evaded`; `Dormant` ⇒ `RouteToHuman` (UNDECIDABLE cause — third conservatism-join cell) |
/// | `Drift` (precision-drop)| autoimmune over-broadening — never `Obsolete`; the bit-3 read stands, but a shape-gone-undefended precision-drop routes to human (not a clean obsolescence) |
/// | `NoDrift` | pass through the streamless bit-3 verdict alone ([`classify`]) |
/// | `UnderPowered` | `RouteToHuman` (conservatism-JOIN) |
///
/// The third conservatism-join cell (ADR-065 amendment): a recall-`Drift` +
/// `Dormant` (shape present, no near-miss) routes to
/// human — NOT the old "VIRTUAL drift / KEEP." The cause is **genuinely undecidable**
/// on the denominator-free `Affinity` rate: [`Affinity::recall`] is a pure fraction
/// (cluster-size divided out at construction; [`LifeEvent::Scored`](crate::learn::life_record::LifeEvent::Scored) carries no count).
/// A recall-rate drop 0.9→0.4 is indistinguishable between churn (denominator shrank,
/// shape alive — KEEP) and evasion (numerator moved, defect mutated — `ReArm`). Routing
/// to human is honest-blind: it surfaces the loud signal ADWIN detected (which the
/// streamless bit-3 axis is blind to) without guessing which cause applies. The cell
/// becomes decidable when `Scored` carries a `cluster_size` count: stable denominator
/// → recall-drop means Evaded; shrinking denominator → means Dormant/churn. Reserved
/// for the `Scored{affinity, cluster_size}` do-later.
///
/// [`ClassVerdict`]: crate::learn::discriminator::ClassVerdict
/// [`ClassVerdict::Obsolete`]: crate::learn::discriminator::ClassVerdict::Obsolete
/// [`ClassVerdict::RouteToHuman`]: crate::learn::discriminator::ClassVerdict::RouteToHuman
/// [`ClassVerdict::Dormant`]: crate::learn::discriminator::ClassVerdict::Dormant
/// [`SilentStatus`]: crate::learn::reader::SilentStatus
/// [`classify`]: crate::learn::discriminator::classify
pub const