NeuralAmpModeler-rs 0.1.0

High-performance Neural Amp Modeler DSP core: WaveNet/LSTM/ConvNet inference, SIMD math (x86-64-v3), .nam/.namb loader, cabinet IR, resampling and noise gate.
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
916
917
918
919
920
921
922
923
924
925
926
927
928
929
930
931
932
933
934
935
936
937
938
939
940
941
942
943
944
945
946
947
948
949
950
951
952
953
954
955
956
957
958
959
960
961
962
963
964
965
966
967
968
969
970
971
972
973
974
975
976
977
978
979
980
981
982
983
984
985
986
987
988
989
990
991
992
993
994
995
996
997
998
999
1000
1001
1002
1003
1004
1005
1006
1007
1008
1009
1010
1011
1012
1013
1014
1015
1016
1017
1018
1019
1020
1021
1022
1023
1024
1025
1026
1027
1028
1029
1030
1031
1032
1033
1034
1035
1036
1037
1038
1039
<!--
SPDX-License-Identifier: Apache-2.0
Copyright (c) 2026 Fábio Henrique de Lima Silva (fhl.bsb@gmail.com) All rights reserved.
-->

# C++ ↔ Rust Parity Audit — NeuralAmpModelerCore × NAM-rs

Ground-truth comparison between the canonical C++ reference, **NeuralAmpModelerCore**
("NAMcore", vendored read-only at `../third-party/NeuralAmpModelerCore/`), and the NAM-rs
Rust engine (`src/`). NAMcore is the **sole source of truth** for parity — the f64 reference
oracle and the NumPy anchor are error-decomposition tools, not arbiters of correctness (see
[§1.2](#12-the-f64-oracle-is-a-decomposition-tool-not-a-gate)).

This document is audited **per architecture, in phases**, by reading the vendored C++ source
line-by-line against the Rust implementation — not by trusting prior write-ups. Each
architecture section carries a verification banner stating what was actually re-checked and
when. **For a single-page triage of what is actually broken vs. what is under control, read
[§7](#7-known-broken-ledger-sabidamente-broken) first.**

## 0. Audit Status

| Architecture                | Status                                                                                                | Section                                   |
|:--------------------------- |:----------------------------------------------------------------------------------------------------- |:----------------------------------------- |
| **LSTM**                    | ✅ Fully Verified — Native f32 weights, bit-exact/sub-1e-11 interop parity vs NAMcore                 | [§2](#2-lstm-architecture)                |
| **WaveNet A1**              | ✅ Fully Verified — Const-generic fast path & dynamic fallback pass canonical golden gates            | [§3](#3-wavenet-a1-architecture)          |
| **WaveNet A2**              | 🟡 Verified Dynamic/Fast paths — 🔴 Flagship (`wavenet_a2_max.nam`) disabled fail-closed at dispatch  | [§4](#4-wavenet-a2-architecture)          |
| **ConvNet**                 | ✅ IDÊNTICO — Paridade Total de Inicialização e Aritmética (prewarm fix elimina transiente de 2.54e-5)| [§6](#6-other-architectures-out-of-scope) |
| Linear / Container / Cabsim | ✅ Verified — Affine linear, SlimmableContainer, and IR Cabsim covered by targeted test suites        | [§6](#6-other-architectures-out-of-scope) |

## 1. Methodology

### 1.1 Two axes of correctness

1. **Interop parity** — does NAM-rs match NAMcore bit-for-bit (within float tolerance)? Verified
   with committed golden vectors (`tests/fixtures/*.bin`, generated by the C++ `render` tool)
   and live cross-validation (`tests/parity/cpp_parity.rs`). All weights are native f32 (weight quantization
   was eliminated; NAMcore never quantized).

2. **Ideal-math fidelity** — how far is NAM-rs from the exact mathematics? Measured against an
   independent f64 reference oracle (`src/testing/reference_oracle/mod.rs`), itself cross-checked
   against a third, independent NumPy f64 implementation. This isolates the genuine precision
   floor, separate from interop drift.

### 1.2 The f64 oracle is a decomposition tool, not a gate

The oracle answers *"how far is the f32 production code from the mathematical ideal?"* — it is
useful for isolating **where** error comes from (quantization vs. activation approximation vs.
structural divergence). It does **not** decide whether NAM-rs is *correct*. Correctness is
decided exclusively by the **C++ golden vectors and live cross-validation**. A change to
production code should never be justified by "matching the oracle" — only by reducing ESR
against the C++ golden. Confusing the two roles has previously caused a real regression to be
introduced while chasing a bug that did not exist (see [§4.5](#45-known-history--do-not-repeat)).
If the oracle and the C++ golden disagree, **the oracle is wrong** and must be fixed to match
C++ — never the other way around.

### 1.3 Reference version

The vendored working copy at `../third-party/NeuralAmpModelerCore/` is checked out at tag
`v0.5.4` (commit `1f42f88`; `NAM/version.h` still says `0.5.3` — the header wasn't bumped for
the tag). Some older committed golden vectors were generated at `v0.5.3` (`9c7b185`). This
patch-level drift is below the interop noise floor for all architectures except where explicitly
noted per-model. Regenerate goldens with `tests/fixtures/golden_gen_build.sh` when in doubt.

### 1.4 Fixture governance: `tests/fixtures/README.md`

[`tests/fixtures/README.md`](../tests/fixtures/README.md) is the canonical operational
supply-chain contract. Every parity claim in this document is operationalized through it.

| Layer                               | Mechanism                                                                                                                          | Hard-fail gate                |
|:----------------------------------- |:---------------------------------------------------------------------------------------------------------------------------------- |:----------------------------- |
| **Layer 0 — Generation pipeline**   | `tests/fixtures/golden_gen_build.sh` + `CATALOG` array — regenerates every `.bin` golden from the pinned NAMcore C++ `render` tool | —                             |
| **Layer 1 — Pre-committed goldens** | `tests/models/golden_vectors.rs` — compares Rust output against committed `.bin` files; no C++ toolchain required                  | `utils/tests-quick.sh` Fase 2 |
| **Layer 2 — Live cross-validation** | `tests/parity/cpp_parity.rs` (`#[ignore]`, run via `utils/tests-long.sh`) — builds C++ `render` tool and compares fresh output     | `utils/tests-long.sh`         |

**Freshness manifest:** `tests/fixtures/.golden_manifest.sha256` contains `sha256` of every
model *and* its golden. Verified as a **hard gate** in `utils/tests-quick.sh` Fase 2 (a stale
`.nam` or `.bin` fails the suite); warn-only in `utils/tests-long.sh`.

**NAMcore mirror pinning:** `variables.env` pins the vendored C++ reference at
`NAM_CORE_COMMIT=1f42f88535884450104b8711d7595019afa0495b` (tag `v0.5.4`). Update
via `utils/mod-update.sh`. See `tests/fixtures/README.md` for the full regeneration walkthrough.

**Calibrated thresholds:** per-model SNR/ESR gates cross-checked by
`tests/models/threshold_calibration.rs` (anti-placebo meta-tests, `// Measured:` provenance
comments). A claim not traceable to a catalog entry + manifest hash + calibrated threshold
is unverified.

---

## 2. LSTM Architecture

Read against `NAM/lstm.h`, `NAM/lstm.cpp`, `NAM/dsp.h`, `NAM/dsp.cpp`, `NAM/activations.h/.cpp`
and the corresponding Rust modules (`src/models/lstm/`, `src/loader/dispatcher/lstm/`,
`src/loader/transpose/lstm.rs`, `src/math/lstm/gates.rs`).

### 2.1 Reference algorithm (`NAM/lstm.cpp`)

A stack of `num_layers` LSTM cells, each processing one audio sample at a time, followed by a
linear head:

```text
for each layer i:
  ifgo = W_i · [input ; hidden_i] + b_i        // ifgo = [input_gate, forget_gate, cell_candidate, output_gate]
  c_i  = sigmoid(forget) * c_i + sigmoid(input) * tanh(cell_candidate)
  h_i  = sigmoid(output) * tanh(c_i)
output = head_weight · h_last + head_bias        // no activation
```

Gate order in the weight/state vectors is fixed: **I, F, G, O** at offsets `0, H, 2H, 3H`
(`lstm.cpp:40-44`).

By default (`Activation::using_fast_tanh = false`, `activations.cpp:16`), the gate
nonlinearities are `sigmoid(x) = 1/(1+exp(-x))` and `tanhf(x)` — both **exact** libm/expf-based
implementations (`lstm.cpp:57-65`, `activations.h:64-67`). The `fast_sigmoid`/`fast_tanh`
rational-approximation branch (`lstm.cpp:46-56`) is only enabled by `Activation::enable_fast_tanh()`,
which is called **only** from `tools/benchmodel*.cpp` — never from the `render` tool used to
generate goldens or run live cross-validation. **The C++ reference used for all LSTM parity
checks always uses exact math**, not its own fast-tanh approximation.

C++ generalizes the topology to arbitrary `in_channels`/`out_channels` and even `num_layers ==
0` (pure passthrough, `lstm.cpp:139-149`, zero-filling extra output channels). In practice every
known `.nam` LSTM model is mono in/out with `num_layers ∈ {1, 2}` — see [§2.6](#26-scope-divergence-mono-only-no-zero-layer-support).

### 2.2 Rust implementation

| C++ (`NeuralAmpModelerCore/`)                                                                       | Rust (`src/`)                                                                                                                                                           | Verdict                                                                                                                                       |
|:--------------------------------------------------------------------------------------------------- |:----------------------------------------------------------------------------------------------------------------------------------------------------------------------- |:--------------------------------------------------------------------------------------------------------------------------------------------- |
| `LSTMCell::process_` gate math (`lstm.cpp:31-66`)                                                   | `math/lstm/gates.rs::fused_lstm_gates_{avx2,avx512}` + `models/lstm/layer_kernels.rs`                                                                                   | ✅ Match — same gate order, same `f·c + i·tanh(g)` / `o·tanh(c')` formulas                                                                    |
| Gate-major weight matrix `[4H × (I+H)]`, row-major (`lstm.cpp:19-21`)                               | `LstmLayer::input_hidden_weights: [[[u16; H]; IH]; 4]`, filled by `read_lstm_weights_into`                                                                              | ✅ Match — verified byte-for-byte against the constructor loop                                                                                |
| Bias `[4H]`, initial hidden `[H]`, initial cell `[H]` read order (`lstm.cpp:22-28`)                 | `read_lstm_layer` reads bias → hidden-init → cell-init in the same order                                                                                                | ✅ Match                                                                                                                                      |
| 2-layer chain: `layers[i].process_(layers[i-1].hidden)` (`lstm.cpp:151-153`)                        | `LstmModel2` software-pipelined chain (`model2.rs`) — layer2 consumes layer1's *previous*-step hidden state, reordered for throughput                                   | ✅ Match — mathematically identical sequential stacking, just reordered for instruction-level parallelism                                     |
| Head: `output = head_weight · h_last + head_bias`, no activation (`lstm.cpp:161-164`)               | `dot_product(..) + head_bias`, computed in **native f32 with Kahan compensation** (`use_f32_head = true` in every loader path) when quantized weights are not requested | ✅ Match (superset — Kahan compensation only *reduces* summation error vs. plain accumulation)                                                |
| `GetPrewarmSamples() = 0.5 × expected_sample_rate` (min 1) (`lstm.cpp:125-132`)                     | `prewarm_samples()` — identical formula (`models/lstm/mod.rs`)                                                                                                          | ✅ Match — **corrects a prior claim** in this document that Rust diverged here; both engines have the same opt-out flag with the same default |
| `DSP::Reset()` calls `prewarm()` only `if GetPrewarmOnReset()` (default `true`) (`dsp.cpp:130-139`) | `NamModel::reset()` calls `prewarm()` only `if self.prewarm_on_reset()` (default `true`)                                                                                | ✅ Match — **corrects a prior claim** in this document that Rust diverged here; both engines have the same opt-out flag with the same default |
| Backbone weights are plain `float` (Eigen), no quantization                                         | Gate weights are native f32, dispatched through f32-only GEMV kernels                                                                                                   | ✅ Match — see [§2.5](#25-native-f32-backbone-weights-and-activation-precision)                                                               |

### 2.3 Weight loading (`.nam` JSON / NAMB)

Layout is `input_hidden_weights[4H×IH] → bias[4H] → hidden_init[H] → cell_init[H]`, repeated per
layer, then `head_weights[H] → head_bias`. This is read identically in
`src/loader/dispatcher/lstm/weights.rs::read_lstm_layer{,_dyn}` and cross-checked against
`src/loader/transpose/lstm.rs` (used for the NAMB `GateMajorLstm` pre-transposed layout). Both
paths were read line-by-line against `LSTMCell`'s constructor (`lstm.cpp:9-29`) — confirmed
identical. `WeightCursor::verify_exhausted()` fails closed if the file has too many or too few
floats for the declared topology.

### 2.4 Catalog dispatch

| `(num_layers, hidden_size)` | Rust type                                            | Alias                              |
|:--------------------------- |:---------------------------------------------------- |:---------------------------------- |
| `(1, 3)`                    | `LstmModel1<3, 4, 12>`                               | `Lstm1x3` (official example model) |
| `(1, 8/12/16/24/40)`        | `LstmModel1<H, H+1, 4H>`                             | `Lstm1x{8,12,16,24,40}`            |
| `(2, 8/12/16/24)`           | `LstmModel2<H, H+1, 2H+H, 4H>`                       | `Lstm2x{8,12,16,24}`               |
| Anything else               | `LstmModelDyn` (heap-allocated, `Vec<LstmLayerDyn>`) | —                                  |

`get_lstm_topology` (`src/loader/nam_json/topology/lstm.rs`) rejects `num_layers > 16` and
`hidden_size > 1024` (DoS guard) but has **no lower bound** — see [§2.6](#26-scope-divergence-mono-only-no-zero-layer-support).

### 2.5 Native f32 backbone weights and activation precision

**Backbone Weight Precision:** NAM-rs uses native `f32` weight storage across all LSTM layers, matching NAMcore's `Eigen::MatrixXf` representation (`NAM/lstm.h:38-39`). Eliminating historical weight quantization removed GEMV dequantization overhead, reducing per-sample latency while ensuring bit-exact interop parity for models such as `BossLSTM-2x8` (ESR = 0.00e0 vs NAMcore).

**Measured Interop Results (Standard Mode):**

| Model         | ESR (vs NAMcore) | ESR (vs f64 Ideal) | SNR (dB) | MR-STFT  | Status                     |
|:------------- |:----------------:|:------------------:|:--------:|:--------:|:-------------------------- |
| BossLSTM-1×16 | 8.50e-12         | 8.90e-13           | 110.7    | 2.80e-05 | ✅ Near-bit-exact          |
| BossLSTM-2×8  | 1.00e-11         | 5.68e-13           | 110.0    | 1.57e-05 | ✅ Bit-exact / noise floor |
| LSTM Official | 7.86e-13         | 2.71e-12           | 121.0    | 3.08e-05 | ✅ Near-bit-exact          |

**Key findings:**

- **BossLSTM-2×8 Parity:** Achieves bit-exact / noise-floor convergence with NAMcore (ESR = 1.00e-11 vs NAMcore, 5.68e-13 vs f64 oracle).
- **BossLSTM-1×16 Precision:** Residual error is dominated by activation function precision at high pre-activation magnitudes, which standard exact-grade math reduces to near-zero (ESR = 8.50e-12).
- **Activation Precision Tradeoff:** `ActivationPrecision::Fast` utilizes Padé [5,4] rational `tanh` (max error ~2.32e-3) and a minimax polynomial `sigmoid` (max error ~4.09e-4) for maximum throughput. `ActivationPrecision::Standard` (universal default) runs exact-grade polynomial exp-based math (error ~2e-7), matching C++ libm precision.

### 2.6 Scope divergence: mono-only, no zero-layer support

C++ `LSTM` generalizes to arbitrary `in_channels`/`out_channels` and `num_layers == 0`
(pass-through). NAM-rs does not:

- `read_lstm_layer::<I, H, IH, H4>` for the first layer always hardcodes `I = 1` — there is no
  code path reading `in_channels`/`out_channels` from the `.nam` config for LSTM (unlike A2's
  `topology/a2.rs`, which explicitly validates `in_channels == 1`). A hypothetical LSTM model
  declaring `in_channels: 2` would be silently processed as mono; the extra channel would never
  be read. No known real-world `.nam` LSTM model does this — NAM is guitar/bass amp modeling,
  always mono — but the assumption is **implicit and unvalidated**, not fail-closed.
- `LstmModelDyn::process_{avx2,avx512,avx512_vnni_bf16}` unconditionally dereference
  `self.layers.as_mut_ptr()` before checking `n_layers > 0` (only a `debug_assert!` guards this,
  which is compiled out in release builds). A `num_layers: 0` model would dereference a pointer
  into an empty `Vec`'s (potentially dangling) allocation — undefined behavior in release,
  panic in debug. C++'s equivalent case returns a well-defined passthrough. This is a real,
  confirmed gap in defensive validation, not a hypothetical: `get_lstm_topology` has no lower
  bound on `num_layers`. Low real-world severity (no trained model has zero layers) but should
  be closed by either rejecting `num_layers == 0` at topology detection or handling it explicitly
  in `LstmModelDyn`.

### 2.7 Measured interop drift

LSTM is the one topology whose interop error grows with **signal length** and **host sample
rate** — the recurrent cell state accumulates error over time.

**Native f32 weights & Standard activation:** BossLSTM-2×8 converges to bit-exact / noise-floor parity with NAMcore at 48 kHz (ESR = 1.00e-11). All models default to `Standard` (exact-grade) activation precision, collapsing interop gaps to near-zero across all catalog architectures.

**F64 Oracle Floors:** The model-specific f64-oracle floors (prewarm-paired, 24k prewarm + 4096 samples)
have been fully measured in Standard mode:

- **BossLSTM-1×16**: ESR vs f64 oracle = **8.90e-13 (SNR 110.7 dB)**
- **BossLSTM-2×8**: ESR vs f64 oracle = **5.68e-13 (SNR 110.0 dB)**

Gate constants (`tests/common/constants.rs`):

| Precision mode | Host rate | ESR cap | Margin over worst measured                        |
|:-------------- |:--------- |:-------:|:------------------------------------------------- |
| Fast           | ≤ 96 kHz  | 0.08    | ~1.3× (vs 6.09e-2 @ 96 kHz)                       |
| Fast           | > 96 kHz  | 0.20    | ~1.4× (vs 1.42e-1 @ 192 kHz)                      |
| Standard       | ≤ 96 kHz  | 0.30    | ~5× the Fast cap (covers the Fast→Standard delta) |
| Standard       | > 96 kHz  | 0.60    | Conservative headroom for 192 kHz recurrent drift |

`LSTM_ESR_LIMIT = 7.0e-3` (`tests/common/constants.rs`) is derived from measured
production-vs-oracle ESR with safety margin. No sample rate is excluded from live cross-validation
to make a gate pass (`live_cross_validation_v2_lstm_*` exercises all five supported rates).

### 2.8 Test coverage and fixture quality

Verified directly against `tests/models/golden_vectors.rs`, `tests/parity/cpp_parity.rs`, and
`tests/parity/reference_oracle_f64.rs`:

- **Golden vectors** (`tests/models/golden_vectors.rs`, precomputed `.bin` from the C++ `render` tool,
  always active — not `#[ignore]`d): `test_golden_vectors_lstm_1x16`, `_lstm_2x8`,
  `_lstm_official`. Backing `.nam` files are **real community-trained models**, not synthetic:
  `BossLSTM-1x16.nam` and `BossLSTM-2x8.nam` (Boss Waza Tube Amp Expander community captures,
  compatible license, committed in `tests/fixtures/models/`), plus `lstm.nam` — NAMcore's own
  official bundled example (`example_models/lstm.nam`, 1×3, matches the `Lstm1x3` alias).
- **Live cross-validation** (`tests/parity/cpp_parity.rs`, `#[ignore]`d — requires the C++ toolchain,
  run via `utils/tests-long.sh`): `live_cross_validation_{,v2_}lstm_{1x16,2x8,official,dyn}` plus
  HF-mode variants. Uses the same real fixtures as the golden vectors.
- **No synthetic LSTM fixture exists in the active suite.** There is no equivalent of WaveNet's
  `BossWN-lite.nam` (obsolete synthetic, see §3.7) for LSTM — every committed LSTM golden is
  backed by a real trained model.
- **`LstmModelDyn`** (the non-catalog fallback) is exercised by `lstm_dyn_test.nam` — a small,
  deliberately non-catalog (hidden size outside `{3,8,12,16,24,40}`) **synthetic** fixture built
  to hit the dynamic dispatch path structurally; it is not a trained amp/pedal capture. This is
  the correct use of a synthetic fixture (topology/dispatch coverage), distinct from tone
  fidelity coverage (which the real Boss captures provide for the catalog path only — the
  dynamic LSTM path currently has **no real-model coverage**, since no known community LSTM
  export uses a non-catalog hidden size).

---

## 3. WaveNet A1 Architecture

Read against `NAM/wavenet/model.h`, `NAM/wavenet/model.cpp`, `NAM/wavenet/detail.h`, `NAM/activations.{h,cpp}`,
and the corresponding Rust modules (`src/models/wavenet/`, `src/loader/dispatcher/wavenet/`, `src/loader/nam_json/topology/wavenet.rs`).

### 3.1 C++ Reference Architecture vs. Rust Const-Generic Catalog

The vendored C++ source does not define separate SKU classes for different channel counts. `nam::wavenet::create_config` (`model.cpp:1227-1241`) has exactly two branches: (1) `a2_fast::is_a2_shape()` for the A2 fast path (§4), and (2) a single generic Eigen-based implementation (`detail::Layer` / `detail::LayerArray` / `WaveNet`) for all other WaveNet models regardless of channel count.

"Standard/Lite/Feather/Nano" (16/12/8/4 channels) are a **NAM-rs-side performance
optimization**: these four channel counts happen to cover the overwhelming majority of
real-world community WaveNet exports, so NAM-rs const-generic-specializes them
(`WaveNetModel<CH, K, HEAD>`) for SIMD throughput, falling back to a heap-allocated
`WaveNetModelDyn` for everything else. This is a legitimate and effective engineering strategy,
but it is **NAM-rs's own catalog, not a mirror of any C++-side concept** — the correct framing is
"const-generic fast path vs. generic fallback for the single C++ generic WaveNet class," not
"C++ SKU X maps to Rust SKU X."

### 3.2 Rust implementation

| C++ (`NeuralAmpModelerCore/`)                                                                                                                                      | Rust (`src/`)                                                                                                                                     | Verdict                                                                                                                                                                       |
|:------------------------------------------------------------------------------------------------------------------------------------------------------------------ |:------------------------------------------------------------------------------------------------------------------------------------------------- |:----------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| `detail::LayerArray::ProcessInner` — rechannel → layer loop → head accumulation → head rechannel (`model.cpp:450-511`)                                             | `WaveNetLayerArray::process_block_internal` (`layer_array.rs`)                                                                                    | ✅ Match — same rechannel → layer cascade → head-accumulate → head-rechannel structure                                                                                        |
| `detail::Layer::Process` — dilated conv + input mixin, sum, activation, optional layer1x1/head1x1 residual+skip (`model.cpp:166-376`)                              | `WaveNetLayer::process_block_internal` (`layer.rs`)                                                                                               | ✅ Match for the **plain case** (no gating, no FiLM, no head1x1/layer1x1 variance) — the only case the const-generic fast path implements (§3.6)                              |
| `WaveNet::process` — condition → layer arrays → head_scale (`model.cpp:744-832`)                                                                                   | `WaveNetModel::process` / `WaveNetModelDyn::process` (`model.rs`, `model_dyn.rs`)                                                                 | ✅ Match for the no-condition_dsp, no-post-stack-head case                                                                                                                    |
| Default activation: exact `tanh`/`sigmoid` (`Activation::using_fast_tanh = false`, never flipped by the `render` tool — same as LSTM, §2.1)                        | `ActivationPrecision::Fast` uses Padé[5,4] tanh / minimax-17 sigmoid (opt-in); `Standard` is exact-grade polynomial exp-based (universal default) | ⚠ Same intentional, bounded divergence documented for LSTM (§2.5); applies identically when `Fast` is active                                                                  |
| `LayerArrayParams::get_receptive_field()` — per-array RF, **summed** across all arrays plus condition_dsp's own prewarm (`model.cpp:417-424`, `model.cpp:616-618`) | `WaveNetModel`/`WaveNetModelDyn` prewarm fill                                                                                                     | ⚠ See §3.5 — analytical fill is mathematically sound for the feedforward case, but the `prewarm_samples()` *getter* under-reports, and `condition_dsp` prewarm has a real bug |

### 3.3 Weight loading

Layout (`rechannel → [conv1d+bias, input_mixin, layer1x1+bias]×N → head_rechannel+bias →
head_scale`, repeated per array) is read identically by
`src/loader/dispatcher/wavenet/{standard,lite,feather,nano}.rs` (catalog) and
`src/loader/dispatcher/wavenet/dynamic.rs` (free geometry) against C++'s `WaveNet::set_weights_`
/ `LayerArray::set_weights_` / `Layer::set_weights_` (`model.cpp:135-164, 525-531, 623-645`).
Confirmed identical read order. `WeightCursor::verify_exhausted()` fails closed on
under/over-provisioned weight files.

### 3.4 Catalog dispatch

`get_wavenet_topology` (`src/loader/nam_json/topology/wavenet.rs`) requires **exactly 2 layer
arrays**, `condition_size ≤ 1`, no gating on either array, and an exact dilation-pattern match to
classify a model as a catalog SKU:

| Channels                                                                                         | Dilation pattern (both arrays)                       | Rust type                |
|:------------------------------------------------------------------------------------------------ |:---------------------------------------------------- |:------------------------ |
| 16                                                                                               | `[1,2,4,...,512]` (both arrays, "Standard" pattern)  | `WaveNetModel<16, 3, 8>` |
| 12                                                                                               | `[1,...,64]` then `[128,...,512,1,...,512]` ("Lite") | `WaveNetModel<12, 3, 6>` |
| 8                                                                                                | same Lite-shaped dilation pattern, CH=8 ("Feather")  | `WaveNetModel<8, 3, 4>`  |
| 4                                                                                                | same Lite-shaped dilation pattern, CH=4 ("Nano")     | `WaveNetModel<4, 3, 2>`  |
| Anything else (any channel count, any array count, gated, `condition_size > 1`, post-stack head) | `WaveNetModelDyn`                                    |                          |

This is a NAM-rs-internal classification only — see §3.1. A `condition_dsp` sub-model is **not**
checked at all during catalog matching (see §3.6): a hypothetical 2-array, ungated, CH=16,
Standard-dilation model that also declares a `condition_dsp` JSON key would still match `Known(Standard)`
and be routed to the fast path, which has **zero `condition_dsp` handling** — it would be silently
dropped. No known real `.nam` model exhibits this combination.

### 3.5 Prewarm: an elegant analytical shortcut, with one real bug

C++'s `DSP::prewarm()` is generic and iterative: it computes `mPrewarmSamples` once at
construction (`condition_dsp`'s own prewarm requirement **plus** the **sum** of every layer
array's receptive field, plus any post-stack head's receptive field, `model.cpp:615-620`), then
literally calls `process()` with zero input that many times (`dsp.cpp:67-101`).

NAM-rs's `WaveNetModel`/`WaveNetModelDyn::prewarm()` does **not** iterate. Because a purely
feedforward causal-conv stack driven by a constant input eventually converges to a constant
output at every layer, `prewarm_internal()` computes that fixed point **analytically in one pass**:
process a single zero-input frame through the rechannel (memoryless, exact for a single frame),
then for each layer in order, replicate that single already-correct value across the entire
history buffer (`copy_within`, `layer_array.rs:97-118`) *before* computing that layer's own
single-frame output — which is therefore also exactly the converged constant, propagating
correctness layer by layer. **This is a genuinely correct, elegant O(layers) alternative to
C++'s O(receptive_field) iteration for the plain feedforward case** — verified by working through
the recursion by hand. It is also why `prewarm_samples()`'s wrong return value (below) has zero
effect on WaveNet's own audio correctness: the trait's `prewarm(&mut self, _num_samples)` override
discards the argument entirely and always runs the full analytical fill regardless of what number
is passed in (`mod.rs:79-83, 107-109`).

Two real issues, though:

- **`prewarm_samples()` under-reports, but is provably inert.** `WaveNetModel::prewarm_samples()`
  returns only `array1.receptive_field_size` (ignoring `array2` entirely), and
  `WaveNetModelDyn::prewarm_samples()` returns only `arrays[0]`'s RF (ignoring all subsequent
  arrays in a multi-array cascade) combined via `.max()` with the condition_dsp's prewarm
  requirement rather than C++'s **sum**. Traced every call site
  (`grep`-verified): the only caller is `loader/build.rs`'s `m.prewarm(m.prewarm_samples().max(2048))`
  at initial load, and WaveNet's `prewarm()` discards that argument (previous paragraph). **No
  functional impact today** — but this is a landmine: if this getter is ever wired to a
  host-facing latency API (matching C++'s `GetPrewarmSamples()`, which real hosts do query for
  plugin latency compensation), it would report a materially wrong number. Should either be
  fixed to the correct sum or removed/marked `#[deprecated]` with a comment explaining why it's
  currently safe to be wrong.
- **`condition_dsp.prewarm` handling:** `WaveNetModelDyn::prewarm_internal()` invokes `cond_dsp.prewarm(cond_dsp.prewarm_samples())` (`model_dyn.rs`), ensuring sub-model recurrent states (e.g. LSTM condition DSPs) settle before frame processing begins, matching C++'s `GetPrewarmSamples()` semantics.

### 3.6 Coverage gap: generic gating/FiLM/head1x1/layer1x1 silently ignored outside A2 shape

C++'s generic `detail::Layer` supports gating (`GatingMode::{NONE,GATED,BLENDED}`), 8 independent
FiLM insertion points, `head1x1`, and `layer1x1` for **any** WaveNet, not just A2-shaped ones
(`model.cpp:976-1151` parses these fields unconditionally for every layer array). NAM-rs's A1
path does not:

- `NamLayerConfig` (`src/loader/nam_json/model.rs:62-108`) parses only the legacy boolean
  `gated` field — there is no parsing of the string-valued `gating_mode` (`"none"/"gated"/"blended"`),
  `head1x1`, `layer1x1`, or any of the 8 FiLM parameter objects.
- The legacy `gated` boolean **is** checked, but only to *disqualify* a model from catalog-SKU
  matching (`topology/wavenet.rs:332-338`) — proving the authors were aware gated
  16/12/8/4-channel models could exist. It is **never checked again** for the `Free`/`WaveNetModelDyn`
  path: `get_wavenet_topology`'s free-geometry branch and `build_wavenet_dynamic_inner` never
  read `layer.gated`, and `WaveNetLayerDyn`/`build_wavenet_array_dyn` have no gating code path at
  all.
- **Confirmed consequence:** a WaveNet model with `gated: true` (or any `gating_mode`/`head1x1`/`layer1x1`/FiLM
  usage) that does not match the A2 shape or the `is_wavenet_a2()` activation heuristic (§4 —
  only triggers for single-array, non-Tanh activation) falls through to the A1 `Free`/`Dynamic`
  path and is **silently processed as if `gated = false`** — no error, no warning, mathematically
  wrong output. This is not fail-closed, unlike the equivalent case for `activation != "Tanh"`,
  which `validate_layer_activations` correctly rejects (§3.2).

No known committed or community fixture currently exercises this gap (gating/FiLM in practice
correlates strongly with the A2 architecture generation), but it is a real, code-verified hole,
not a hypothetical — the correct fix is either to parse and honor these fields generically in
`WaveNetModelDyn`, or to detect and reject them explicitly (fail-closed) the same way `gated` is
already detected for catalog-disqualification purposes.

### 3.7 Test coverage and fixture quality

Verified directly against `tests/models/golden_vectors.rs`, `tests/parity/cpp_parity.rs`, and
`tests/parity/reference_oracle_f64.rs`:

- **Golden vectors, catalog SKUs — all real community models, all active (not ignored):**
  `test_golden_vectors_wavenet_{standard,feather,nano}` use `BossWN-{standard,feather,nano}.nam`
  (Boss Waza Tube Amp Expander community captures, compatible license). `test_golden_vectors_wavenet_lite`
  uses `EVH-5150-Lite.nam`, a **non-distributable real community capture** (gitignored, lives in
  `tests/fixtures/models-nondist/`, fetched separately — see `tests/fixtures/README.md`
  §Non-Distributable Model Management). Its doc comment (`golden_vectors.rs:495-511`) and the
  test itself confirm the measured result **directly from current source**: SNR = 122.3 dB, ESR
  = 5.84e-13, thresholds SNR ≥ 105 dB / ESR ≤ 3.5e-11 — this specific figure is **independently
  re-confirmed in this audit pass**, not carried over.

- **Golden vectors, non-catalog:** `test_golden_vectors_wavenet_dyn` (`wavenet_dyn_free.nam`) and
  `test_golden_vectors_wavenet_condition_dsp` (`wavenet_condition_dsp.nam`) are **synthetic**,
  purpose-built to exercise `WaveNetModelDyn`'s free-geometry and `condition_dsp` dispatch paths
  structurally. `test_golden_vectors_wavenet_official` uses `wavenet_official.nam` — NAMcore's own
  bundled official example (`example_models/wavenet.nam`, CH=3, 2 arrays), a small but **real,
  officially-distributed** reference model, not synthetic.

- **Obsolete synthetic fixture, kept for traceability only:** `BossWN-lite.nam` (CH=12,
  artificially generated) is explicitly marked obsolete in `tests/fixtures/README.md` — "no
  longer used in active tests," superseded by `EVH-5150-Lite.nam`. It is the historical source of
  the "SNR ≈ 0.9 dB" figure that `docs/testing.md` still (incorrectly) attributes to the current
  active test.

- **Live cross-validation** (`tests/parity/cpp_parity.rs`, `#[ignore]`d, requires C++ toolchain):
  `live_cross_validation_{,v2_}wavenet_{standard,feather,nano,lite,a1_standard,dyn}` plus HF
  variants, using the same real fixtures above. `live_cross_validation_nondist_models` and the
  named `live_cross_validation_v2_{app_evh,boss_bd2,slammin_marshall}` tests exercise **three
  additional real, non-distributable community captures** (`APP-EVH-Stealth100-Dialled-xSTD.nam`,
  `Boss BD-2 H2O Mod T-12_00 G-12_00.nam`, `SLAMMIN_MARSHALL_J45_VN9_TREBLEBOOSTER_P4_C.nam` —
  catalogued with SHA-256 + author attribution in `tests/fixtures/models-nondist/manifest.json`),
  covering custom-layer WaveNet and `SlimmableContainer` topologies beyond the four catalog SKUs.
  These gracefully no-op (printed `SKIP`, not a failure) when the non-distributable directory or
  the C++ toolchain is absent, consistent with `docs/testing.md`'s documented graceful-skip
  policy.

- **Net assessment:** WaveNet A1's test fixture quality is materially better than a synthetic-only
  suite would suggest — every catalog SKU and several custom real-world topologies are validated
  against genuine trained amp/pedal captures, not just structurally-synthetic weights. The
  remaining synthetic fixtures (`wavenet_dyn_free.nam`, `wavenet_condition_dsp.nam`) are correctly
  scoped to structural/dispatch coverage rather than claiming tone-fidelity validation.

### 3.8 Measured interop drift

Canonical golden-vector interop fidelity measured against NAMcore (`docs/quality-contract.txt` baseline @ 48 kHz):

| Model                   | ESR (vs NAMcore) | ESR (vs f64 Ideal) | SNR (dB) | MR-STFT  | Mode |
|:----------------------- |:----------------:|:------------------:|:--------:|:--------:|:---- |
| WaveNet Standard (CH16) | 2.31e-14         | 9.05e-15           | 136.4    | 6.46e-06 | Live |
| WaveNet Feather (CH8)   | 4.74e-14         | 2.00e-14           | 133.2    | 8.86e-06 | Live |
| WaveNet Nano (CH4)      | 6.43e-14         | 3.05e-14           | 131.9    | 7.67e-06 | Live |
| EVH-5150-Lite (CH12)    | 7.87e-13         | 2.64e-13           | 121.0    | 4.31e-06 | Live |
| WaveNet A1 Standard     | 1.20e-13         | 1.05e-13           | 129.2    | 2.26e-06 | Live |
| WaveNet Official (CH3)  | 9.03e-14         | 6.13e-14           | 130.4    | 1.66e-05 | Live |

All WaveNet A1 catalog models pass their calibrated quality gates with multi-order-of-magnitude safety margins.

### 3.9 `condition_dsp` specification (canonical semantics)

> This section is the formal specification. It was derived by
> reading the C++ reference, the Python trainer, and the Rust production code side-by-side
> on 2026-07-14. All file:line citations reference NAMcore v0.5.4 (tag `1f42f88`).

#### 3.9.1 C++ semantics — `WaveNet::_process_condition` and sizing

**Source:** `../third-party/NeuralAmpModelerCore/NAM/wavenet/model.cpp`

The `condition` matrix flowing through the WaveNet layer cascade is `_condition_output`
(`Eigen::MatrixXf`, `model.h:76`). Its dimensions are decided in `SetMaxBufferSize`
(`model.cpp:647-687`):

- **Without `condition_dsp`** (`model.cpp:652-654`): `_condition_output` is resized to
  `[_get_condition_dim(), maxBufferSize]`. `_get_condition_dim()` returns
  `NumInputChannels()` (`model.h:106`), which is always **1** for WaveNet (mono-in).
  So `_condition_output` = `[1 × maxBufferSize]` — a single row holding the raw input.

- **With `condition_dsp`** (`model.cpp:656-660`): `_condition_output` is resized to
  `[condition_dsp->NumOutputChannels(), maxBufferSize]`. The number of **rows** in the
  condition matrix is the condition DSP's output channel count, **not** the WaveNet's
  `in_channels`.

The `_process_condition` method (`model.cpp:699-729`) fills `_condition_output`:

- **Without `condition_dsp`** (`model.cpp:703-704`): copies `_condition_input.leftCols(num_frames)`
  into `_condition_output.leftCols(num_frames)`. Both are `[1 × num_frames]`.

- **With `condition_dsp`** (`model.cpp:710-728`):

  1. Input (`_condition_input`, shape `[condition_dim, num_frames]`, where
     `condition_dim = _get_condition_dim() = 1`) is copied row-by-row into
     pre-allocated contiguous DSP buffers (`model.cpp:710-715`).
  2. The condition DSP processes these buffers in-place (`model.cpp:718-719`).
  3. Output is copied back row-by-row from the DSP output buffers into
     `_condition_output` (`model.cpp:722-727`). The row count is
     `condition_dsp->NumOutputChannels()` — there is **no** broadcast, tile, or
     dimension coercion. Output channels are written 1:1.

**Construction-time validation** (`model.cpp:592-601`): when `condition_dsp` exists, C++
asserts that **every** layer array's `condition_size` matches
`condition_dsp->NumOutputChannels()` exactly — and throws `std::runtime_error` on mismatch:

```cpp
// model.cpp:594-601
if (layer_array_params[i].condition_size != this->_condition_dsp->NumOutputChannels())
{
    std::stringstream ss;
    ss << "condition_size of layer " << i << " ("
       << layer_array_params[i].condition_size
       << ") doesn't match output channels of condition DSP ("
       << this->_condition_dsp->NumOutputChannels() << "!\n";
    throw std::runtime_error(ss.str().c_str());
}
```

**Conclusion:** In C++, a model where `condition_dsp->NumOutputChannels() < condition_size`
is **rejected at construction**. The case `out_channels == condition_size` is guaranteed
by this check. There is no broadcast logic — dimensional matching is enforced structurally.

#### 3.9.2 How `_condition_output` is consumed by layers

In `WaveNet::process` (`model.cpp:744-832`):

- `_condition_output` (the multi-row condition matrix) is passed **as-is** to every
  layer array's `Process` method (`model.cpp:761,770`), alongside the layer inputs.
- Inside each `Layer::Process` (`model.cpp:166+`), the condition is consumed by
  `InputMixer` — a `Conv1D(kernel=1, in_channels=condition_size, out_channels=mid_channels)`
  — which projects `condition_size` channels to `mid_channels` (= `2*bottleneck` for
  gated, `bottleneck` for plain). FiLM modules also consume the condition channels
  directly.
- **No broadcasting between `_condition_output` and the layer internals.** The matrix
  already has the correct row count by construction (§3.9.1).

#### 3.9.3 Python trainer semantics (`neural-amp-modeler`, v0.13.0)

**Source:** `nam/models/wavenet/_wavenet.py` (tag `v0.13.0`)

The trainer's `WaveNet.parse_config` (`_wavenet.py:142-155`) handles `condition_dsp`:

```python
if condition_dsp_config.get("name") != "WaveNet":
    raise NotImplementedError("Only WaveNet condition DSP is supported")
condition_dsp = WaveNet.init_from_config(condition_dsp_config["config"])
```

- The Python trainer **only supports WaveNet as `condition_dsp`** — any other
  architecture (including LSTM) raises `NotImplementedError`.

- During training (`forward`, `_wavenet.py:189`):

  ```python
  c = x if self._condition_dsp is None else self._condition_dsp(x)
  ```

  The condition tensor `c` has shape `[B, condition_dsp_head_out_channels, L]` — exactly
  the head output of the condition-dsp WaveNet.

- Export (`export_config`, `_wavenet.py:176-195`): the `condition_dsp` sub-model is
  serialized as a complete `.nam` JSON object embedded inside the parent model's config
  under the `"condition_dsp"` key. The note at line 192 reads:

  ```python
  # Build condition_dsp export dict without running forward (condition_dsp
  # may have multiple output channels; WaveNet wrapper asserts 1 channel).
  ```

**Conclusion:** The Python trainer's `condition_dsp` output channels match the
`condition_size` of the parent's layer arrays by the trainer's own structural design
(the condition-dsp WaveNet's `head.out_channels` = layer arrays' `condition_size`).
There is **no** code path in the official trainer that produces a `condition_dsp`
output-channel-count mismatch — it would fail dimension checks during forward
computation.

#### 3.9.4 LSTM as `condition_dsp` — veredicto

The `wavenet_condition_lstm.nam` fixture (LSTM sub-model inside a WaveNet) represents
a configuration that:

1. **The Python trainer cannot produce** — raises `NotImplementedError` for non-WaveNet
   `condition_dsp` (§3.9.3).
2. **C++ NAMcore would reject at construction** — the LSTM's `NumOutputChannels() = 1`
   would fail the assertion `layer_array.condition_size == condition_dsp->NumOutputChannels()`
   when `condition_size = 3` (the standard WaveNet case) (§3.9.1).
3. **The C++ `render` tool does not support** this model — there is **no golden vector**
   generated by NAMcore for this fixture. The only committed golden for this model is the f64 oracle — which itself has a disputed `condition_dsp` semantic
   (the broadcast logic at `wavenet.rs:38-49` and `a2/dynamic_eval.rs:326-339`). This creates a circular
   dependency: the oracle's correctness for this fixture cannot be independently validated.

Rust production code behavior (`src/models/wavenet/model_dyn.rs:236-251`): when
`condition_dsp` output channels (`dsp_ch`) are fewer than the layer array's
`condition_size` (`cond`), the production engine **broadcasts** the first channel's
value across all condition channels. This broadcast is present in both the A1
dynamic path (`model_dyn.rs:240-247`) and the A2 dynamic/cascade paths (via the
same `condition_dsp_output` buffer). This is a **NAM-rs-specific behavior** with
no C++ precedent — it exists because NAM-rs loads models the upstream toolchain
rejects.

**Recommendation for T1.2 (oracle fix):** The f64 oracle's broadcast logic
(`wavenet.rs:38-49`, `a2/dynamic_eval.rs:326-339`) should match the production code's broadcast
**if** the production broadcast is deemed the intended semantics for models the
upstream toolchain cannot validate. Since the C++ golden cannot serve as arbiter
for the LSTM case, the "correct" broadcast behavior is a product decision documented
here — not a parity claim.

#### 3.9.5 Summary: canonical `condition_dsp` semantics

| Aspect                                 | C++ (NAMcore v0.5.4)                                                        | Python trainer (v0.13.0)                               | Rust production (NAM-rs)                                     |
|:-------------------------------------- |:--------------------------------------------------------------------------- |:------------------------------------------------------ |:------------------------------------------------------------ |
| `condition_dsp` matrix rows            | `condition_dsp->NumOutputChannels()`                                        | `condition_dsp.head.out_channels`                      | `condition_dsp.num_output_channels()`                        |
| Dimension enforcement                  | Hard assertion: `condition_size == NumOutputChannels()` (throw on mismatch) | Structural match (fails dimension check in forward)    | `assert` on max channels; broadcasts when `dsp_ch < cond`    |
| Broadcasting (dsp_ch < cond)           | **None** — construction rejected                                            | **None** — structural match prevents mismatch          | **Yes** — replicates channel 0 across all condition channels |
| Supported condition_dsp architectures  | Only WaveNet (and LSTM — but see §3.9.4 re: assertion rejection)            | Only WaveNet (raises `NotImplementedError` for others) | Any (LSTM accepted; see §3.9.4)                              |
| Reference for `condition_lstm` fixture | N/A — model rejected                                                        | N/A — model cannot be produced                         | Golden from `wavenet_condition_dsp.nam` (WaveNet sub-model)  |
| Key file:line references               | `model.cpp:592-601,652-660,699-729,744-770`                                 | `_wavenet.py:142-155,171-195`                          | `model_dyn.rs:236-251`, `model_dyn.rs:357-373`               |

#### 3.9.6 T6.1 Root Cause: `head_scale` read from JSON config instead of weight stream (WaveNet A1 oracle)

**Status:** FIXED (2026-07-14, T6.1).

**Root cause:** Both the Rust oracle (`src/testing/reference_oracle/wavenet.rs:33`)
and the Python anchor generator (`tests/fixtures/scripts/validate_oracle_f64.py:93`)
read `head_scale` from the JSON config field `model_data.config.head_scale`, not from
the last position of the weight stream — where production engines (Rust `build_wavenet_dynamic_inner`,
C++ `NAM/wavenet/model.cpp`) always read it.

For models generated by standard NAM trainers, the config `head_scale` and the
weight-stream `head_scale` are the same value, so the bug is hidden. For
test-script-generated models (`create_wavenet.py`), the weight stream may contain
random values that overwrite the config metadata. This caused the f64 oracle to
produce structurally wrong output for `wavenet_condition_dsp.nam`:

**`wavenet_condition_dsp.nam` — before vs. after T6.1:**

| Measurement                               | Before T6.1 (broken oracle) | After T6.1 (fixed oracle) |
|:----------------------------------------- |:--------------------------- |:------------------------- |
| Prod × Oracle ESR (paired, summary table) | 4.23e+01 (+16.3 dB)         | 6.33e-15 (−142.0 dB)      |
| Oracle × NumPy anchor ESR                 | N/A (circular: 4.96e-16)    | 3.18e-32 (−315.0 dB)      |
| Quality Dashboard tag                     | `[orac: f64 div]` TRIGGERED | **not triggered**         |
| Prod output (first 10)                    | ≈ +0.17 growing             | ≈ +0.17 growing           |
| Oracle output (first 10)                  | ≈ −0.033 flat               | ≈ +0.17 growing (matches) |

**What was wrong:** The main model's weight-stream head_scale = −0.1255 (the actual
weight at position 146), but the oracle used config head_scale = 0.02 — a sign inversion
and 6.27× magnitude error. The condition_dsp sub-model's weight-stream head_scale =
0.8649, but the oracle used 0.02 again — a 43.25× magnitude error. The combination of
both mismatches produced oracle output that was structurally unrelated to production.

**Fix (2 files, 2 languages):**

1. **Rust oracle** (`src/testing/reference_oracle/wavenet.rs:161-167`): After reading
   all array weights, read the last remaining weight from the cursor as `head_scale`.
   The config field is no longer used for computation.

2. **Python anchor generator** (`tests/fixtures/scripts/validate_oracle_f64.py:195-199`):
   Same fix — read `head_scale` from `weights[cursor]` after the per-array weight
   loop. The config field is no longer used for computation.

**Verification:** `test_summary_table` now shows ESR(WaveNetCondDSP) = 6.33e-15 (−142.0 dB),
matching the near-bit-exact floor of the WaveNet A1 family (1e-14 to 1e-12 range).
The regenerated Python anchor matches the Rust oracle at 3.18e-32 ESR — both now
read head_scale from the same weight-stream position and agree with the production
engine (itself golden-C++-confirmed at ESR 1.11e-14).

**Status after T6.1:**

- ✅ `wavenet_condition_dsp.nam` — oracle verified against production at the A1 floor
- ✅ Python anchor regenerated and validated against production, NOT circularly
- ✅ Quality Dashboard `[orac: f64 div]` tag eliminated for this model
- ✅ `docs/cpp_parity_map.md` §3.9 now records the definitive root cause

---

## 4. WaveNet A2 Architecture

> Read against `NAM/wavenet/a2_fast.{h,cpp}` (the C++ fast-path, in full) and cross-checked
> `src/loader/nam_json/topology/a2.rs`, `src/models/a2/model/static/process.rs`,
> `tests/models/golden_vectors.rs`, `tests/parity/cpp_parity.rs`, `tests/common/validation.rs`, and
> `tests/fixtures/README.md` against each other and against current git history.
> §4.4–§4.6 (the `wavenet_a2_max.nam` investigation) were already
> established in the previous pass and are corroborated, not re-derived, here.

"A2" designates the newer WaveNet variant: `a2_fast.cpp` is C++'s **optimized, shape-restricted**
fast path (exactly 23 layers, fixed kernel/dilation pattern, CH∈{3,8}, LeakyReLU-only, no
gating/FiLM/head1x1 — `a2_fast.cpp:754-885`); anything not matching that exact shape falls
through to the same generic `NAM/wavenet/model.cpp` used by A1 (§3.1). NAM-rs mirrors this split
faithfully: `WaveNetA2<3>`/`WaveNetA2<8>` (fast path) vs. `WaveNetA2Dyn`/`WaveNetA2Cascade`
(everything else — FiLM, gating, blending, `condition_dsp`, multi-array cascade, `head1x1`).

### 4.1 Fast-path shape detection: a faithful, self-correcting mirror of C++

`src/loader/nam_json/topology/a2.rs::is_a2_shape` was read line-by-line against
`a2_fast.cpp::is_a2_shape` (lines 754-885) — every one of the 20 structural checks (layer count,
no post-stack head, `in_channels`/`input_size`/`condition_size`, `channels == bottleneck`,
`channels ∈ {3,8}`, exact kernel-size/dilation arrays, LeakyReLU(0.01) activation, gating,
head1x1, layer1x1 groups, layer-array head shape, all 8 FiLM slots, `groups_input*`,
non-slimmable) has a corresponding Rust check, in the same order, with a comment citing the C++
line number. This is the best-audited topology detector in the codebase.

**Notably, the code contains its own documented self-correction** (`topology/a2.rs:207-213`,
tagged `B.1.1 (F5)`): an earlier version of the Rust dispatcher apparently routed FiLM-active
models to the fast path anyway, producing measured divergence (CH=3 SNR 18.1 dB, CH=8 SNR 36.0
dB) against C++ — because C++'s `is_a2_shape` rejects any active FiLM slot and falls through to
the generic Eigen WaveNet, which the Rust fast path does not reproduce. The fix (already in the
current source) routes any model with FiLM, gating, `head1x1`, or non-1 groups to
`A2TopologyResult::Dynamic` instead, matching C++'s fallback exactly. This is now correct — but
the 18.1/36.0 dB figures remain the calibrated thresholds for the *dynamic* engine's FiLM
emulation itself (§4.2), since matching the shape-routing decision doesn't yet mean matching the
generic Eigen path's output bit-for-bit.

### 4.2 Fast path (A2-Full CH=8 / A2-Lite CH=3): structurally correct, only synthetic fixtures

`src/models/a2/model/static/process.rs` was read against `a2_fast.cpp`'s `A2FastModel<Channels>`:
rechannel → per-layer (dilated conv → bias → input mixin → LeakyReLU(0.01) → head-accumulate →
`layer1x1` residual) → head Conv1D(k=16, bias, `head_scale`). Structurally identical, including
weight-stream read order (`_load_weights`, `a2_fast.cpp:198-273`) matching
`src/models/a2/model/set_weights.rs` field-for-field.

Re-measured 2026-07-11 (`utils/tests-quick.sh` Fase 2, release): A2-Full ESR = 1.12e-13
(SNR 129.5 dB), A2-Lite ESR = 6.43e-14 (SNR 131.9 dB) against the committed NAMcore
goldens — both pass their calibrated gates (3.0e-11 / 3.5e-11, SNR ≥ 105 dB) with
2+ orders of magnitude of margin.

**Fixture quality caveat (new finding — see §4.6):** unlike LSTM and WaveNet A1, these figures are
**not** validated against a real trained community model. `wavenet_a2_full.nam` and
`wavenet_a2_lite.nam` are explicitly documented as **synthetic, calibrated weights** — `tests/fixtures/README.md:75-76,
520-527`: "Synthetic, NOT official FiLM models." There is currently no known real-world A2-Full
or A2-Lite `.nam` export in the test suite. The fast-path *code* is well-verified against C++
structurally; the fast-path *fixtures* only prove self-consistency of calibrated weights, not
tone-fidelity on a genuine trained model.

### 4.3 Measured interop drift on dynamic paths (Gating, Blending, FiLM)

Measured interop metrics for WaveNet A2 dynamic paths (`docs/quality-contract.txt` baseline @ 48 kHz):

| Model / Variant                     | ESR (vs NAMcore) | ESR (vs f64 Ideal) | SNR (dB) | MR-STFT  | Mode |
|:----------------------------------- |:----------------:|:------------------:|:--------:|:--------:|:---- |
| WaveNet A2-Full (CH8)               | 1.46e-13         | 7.83e-14           | 128.3    | 1.68e-05 | Live |
| WaveNet A2-Lite (CH3)               | 8.36e-14         | 1.82e-14           | 130.8    | 9.54e-06 | Live |
| WaveNet A2-FiLM-Full (CH8)          | 1.18e-14         | 8.75e-15           | 139.3    | 7.85e-06 | Live |
| WaveNet A2-FiLM-Lite (CH3)          | 3.82e-13         | 1.61e-13           | 124.2    | 1.69e-05 | Live |
| WaveNet A2-FiLM-InputMixinPre (CH3) | 3.44e-14         | 2.21e-14           | 134.6    | 6.92e-06 | Live |
| WaveNet A2-FiLM Chaos Stress (CH3)  | 1.26e-14         | 1.03e-14           | 139.0    | 7.00e-06 | Live |
| WaveNet A2 Dynamic Gated (CH8)      | 5.03e-11         | 1.00e-10           | 103.0    | 6.63e-05 | Live |
| WaveNet A2 Dynamic Blended (CH3)    | 5.35e-14         | 2.65e-14           | 132.7    | 9.97e-06 | Live |

All dynamic path variants achieve near-bit-exact parity or expected approximation-bounded floors.

### 4.4 🔴 Status: wavenet_a2_max.nam (Official Flagship) — Disabled Fail-Closed

This model is the sole item in the A2 topology currently disabled fail-closed at dispatch (`is_disabled_broken_a2_flagship` in `src/loader/dispatcher/wavenet/mod.rs`).

**Root Cause Summary:**
Three structural bugs in the dynamic engine contribute to the divergence against the C++ golden vector (`golden_wavenet_a2_max.bin`):

- **Bug A:** `head1x1` is modeled as per-array in Rust, whereas C++ evaluates it per-layer.
- **Bug B:** Grouped-convolution parameters (`layer1x1.groups` and `groups_input_mixin`) are ignored by the dynamic path.
- **Bug C:** Legacy head kernel size (`kernel_size=1`) is processed with hardcoded 16-tap kernel assumptions.

Detailed fix plan and status tracking are maintained within the WaveNet A2 dynamic engine architecture specifications.

### 4.5 Known history — do not repeat

A prior audit round compared production output (`condition_size=8` values/frame) against the
f64 oracle's `condition_dsp` output (1 value/frame — a bug in the oracle, not production) and
concluded there was a critical 93 dB regression. Acting on that conclusion, it changed
production code to match the broken oracle, which reintroduced a real divergence from C++
that a prior fix had already corrected. That change was reverted. The load-bearing rule: **the C++ golden is the only arbiter; the oracle decomposes error but never adjudicates it.**

### 4.5.1 Anchor Regeneration Policy

A f64 anchor (`tests/fixtures/f64_anchors/*.bin`) may only be regenerated when:

**(a)** a C++ golden vector exists for the same fixture **and** the paired
production×oracle test (`test_summary_table`) already passes within the
acceptance criterion **before** regeneration; **or**

**(b)** no C++ golden exists and the regeneration is accompanied by explicit
human review, documented in the commit message with before/after numbers.

Regenerating an anchor from the very oracle it is meant to validate constitutes
a circular comparison and does **not** constitute evidence of correctness.

### 4.6 Canonical C++ Layout Specifications

The structural spec table below documents C++ reference structures vs Rust implementations for ongoing dynamic engine parity work:

| Aspect                                               | C++ reference                                                                            | Rust reference                                                                                   | Verdict                                                                    |
|:---------------------------------------------------- |:---------------------------------------------------------------------------------------- |:------------------------------------------------------------------------------------------------ |:-------------------------------------------------------------------------- |
| `Conv1x1` weight stream order                        | `NAM/dsp.cpp:384-393` — row-major `[out_ch][in_ch]` per group                            | `src/models/a2/model/dynamic/build.rs` (`transpose_dense_f32` + `head1x1_w[oc*h1_in+ic]` access) | Tested both transposed and non-transposed variants against the golden      |
| `head1x1` weight count                               | `NAM/wavenet/detail.h:75-76` — `out_channels × (bottleneck/groups)`, bias `out_channels` | `build.rs` reads `head_accum_size × h1_in_size` (`head_accum_size == out_channels`)              | ✅ Matches C++ formula                                                     |
| `head1x1` application loop (grouping)                | `NAM/dsp.cpp:449-646` — implicit block-diagonal GEMM                                     | `process.rs` explicit `grp → oc → ic` loop                                                       | ✅ Structurally equivalent                                                 |
| Cascade head propagation (multi-array)               | `NAM/wavenet/model.cpp:769` — propagates **post-rechannel** head output                  | `cascade.rs` propagates **raw** `head_accum` (pre-rechannel)                                     | ⚠ Latent divergence for multi-array models with `head_kernel_size > 1`     |
| `condition_dsp` interface (dimensions, pass-through) | `NAM/wavenet/model.cpp:699-729`                                                          | `process.rs:89-98`                                                                               | ✅ Interface dimensions match (`condition_size` values/frame)              |
| Head finalization, `head_size == 1`                  | `NAM/wavenet/model.cpp:382-383` — Conv1D(kernel=head_kernel_size, bias, head_scale)      | `A2HeadConv` (kernel=16, bias, head_scale)                                                       | ✅ Matches for `wavenet_a2_max.nam` (`head_size=1`, `kernel=16`)           |
| Head finalization, `head_size > 1`                   | Conv1D with kernel + bias + head_scale                                                   | Dense projection, no kernel/bias/head_scale                                                      | ⚠ Only equivalent to C++ when `head_kernel_size == 1 ∧ head_bias == false` |

### 4.7 Test coverage and fixture quality

Verified directly against `tests/models/golden_vectors.rs`, `tests/parity/cpp_parity.rs`,
`tests/common/validation.rs`, and `tests/parity/reference_oracle_f64.rs`:

- **`tests/models/golden_vectors.rs`** (committed `.bin`) covers `golden_wavenet_a2_lite.bin` (Lite
  variant, `condition_size=1`, CH=3) and `golden_wavenet_a2_full.bin` (Full variant,
  `condition_size=1`, CH=8). Both pass bit-identically against prior baselines.

- **`tests/parity/cpp_parity.rs` (live, `#[ignore]`d)** has no
  `live_cross_validation_wavenet_a2_dyn` test because `WaveNetA2Dyn`'s scalar fallback path is
  a `nam-rs` internal extension for non-standard geometries not present in upstream C++ NAMcore.
  Cross-validation uses the synthetic dynamic builder anchor tests instead.

- **Real, official `.nam` files exercised:** exactly two.

  - `a2_example.nam` — NAMcore's own official bundled example (`example_models/A2.nam`,
    `SlimmableContainer` with two WaveNet A2 submodels, CH 3→6). Golden-tested
    (`test_golden_vectors_a2_example_slimmable`) and live-cross-validated
    (`live_cross_validation_a2_example_slimmable`). Passes.
  - `wavenet_a2_max.nam` — Steve Atkinson's official flagship example (CC0). Golden-tested only
    (`test_golden_vectors_wavenet_a2_max`, `#[ignore]`d) — **confirmed broken**, §4.4. **No live
    cross-validation exists for this model at all** — `tests/parity/cpp_parity.rs` has no
    `live_cross_validation_*wavenet_a2_max*` test, so even once the golden is fixed, the "fresh
    C++ toolchain" axis (§1.1) will remain uncovered for this model unless a live test is added.

- **Every other A2 fixture is synthetic**, by explicit design and documentation
  (`tests/fixtures/README.md`), not by omission:

  - `wavenet_a2_full.nam` / `wavenet_a2_lite.nam` (fast-path parity, calibrated weights) — explicitly
    labeled "**NOT official FiLM models**" to prevent future confusion with `wavenet_a2_max.nam`.
  - `wavenet_a2_film_{full,lite}.nam` (FiLM dynamic path), `a2_dynamic_gated_ch8.nam` /
    `a2_dynamic_blended_ch3.nam` (gating/blending dynamic path), `wavenet_a2_container.nam`
    (`SlimmableContainer` joining the two fast-path submodels) — all generator-produced
    (`generate_a2_fixtures.py` for the FiLM pair), purpose-built to exercise one structural
    feature each against the C++ **generic** path (not `a2_fast`, which rejects all of them —
    §4.1). This is the correct, honest use of synthetic fixtures: proving the *feature* works,
    not claiming tone-fidelity on a trained model.
  - `mock_a2.nam` — a deliberate negative fixture (zero weights, `ReLU` config) used only to test
    the RT-safe model-load-failure path (`RT_STATUS_MODEL_LOAD_FAILED`), not inference at all.

- **Net assessment:** A2's test coverage is real for the *shape-detection* logic (§4.1, mirrored
  line-by-line from C++) and for the *dynamic-engine feature* fixtures (gating/blending
  near-bit-exact, FiLM characterized at 18–36 dB), but it is the **weakest of the three
  architectures on genuine trained-model validation** — the only real official A2 model exercised
  by live cross-validation is a `SlimmableContainer` wrapper, and the only real official
  flagship WaveNet A2 model (`wavenet_a2_max.nam`) is both broken and has zero live-toolchain
  coverage.

---

## 5. Shared DSP Engine Semantics

Applies identically to LSTM, WaveNet A1, and A2 — all route through the common `NamModel` trait
and the C++ `DSP` base class.

| C++ (`NAM/dsp.h` / `dsp.cpp`)                                                                                              | Rust (`src/`)                                                                                                                                                                                                                        | Verdict                                                                                                                                                                                                                                                                                          |
|:-------------------------------------------------------------------------------------------------------------------------- |:------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------ |:------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------ |
| `DSP::Reset(sr, maxBuf)` → `SetMaxBufferSize` + `prewarm()` iff `GetPrewarmOnReset()` (default `true`)                     | `NamModel::reset()` → `set_max_buffer_size` + `prewarm()` iff `prewarm_on_reset()` (default `true`)                                                                                                                                  | ✅ Match — verified for LSTM (§2.2) and WaveNet A1 (§3.5); A2 not re-verified this pass                                                                                                                                                                                                          |
| `DSP::GetPrewarmSamples()` base returns `0`; overridden per-model; used by the **iterative** `DSP::prewarm()` loop         | `prewarm_samples()` per-model override                                                                                                                                                                                               | ✅ LSTM verified exact (`0.5 × sr`), and its value is load-bearing (drives real iteration). ⚠ WaveNet A1's override under-reports (§3.5) but is **provably inert** — WaveNet's `prewarm()` discards the argument and uses its own correct analytical fill instead. A2 not re-verified this pass. |
| `Activation::using_fast_tanh` default `false` (exact `tanh`/`sigmoid`); only flipped by benchmark tools, never by `render` | Activation precision selected via `ActivationPrecision::{Fast, Standard}`; `Fast` uses Padé/minimax approximations, not exact math. `Standard` (exact-grade polynomial, universal default) matches C++ exact math parity within 2e-7 | ⚠ **Intentional divergence, not a bug.** C++'s reference path used for goldens is exact math; NAM-rs's `Fast` mode trades a small, bounded approximation error for throughput. `Standard` (exact-grade default) narrows this to identical parity within measurement noise (§2.5).                |

### 5.1 Sample Rate Default Policy (F-P3)

**Background:** the C++ NAMcore uses `NAM_UNKNOWN_EXPECTED_SAMPLE_RATE = -1.0`
(`NAM/dsp.h:30`) as a sentinel when the `sample_rate` field is absent from the `.nam`
JSON. When `expected_sample_rate == -1.0`, the LSTM prewarm computation
(`NAM/lstm.cpp:128`) produces `max(1, (int)(0.5 × -1.0)) = 1` sample — effectively
disabling prewarm.

**NAM-rs policy:** `sample_rate` absence defaults to **48000 Hz**
(`src/loader/loaded_model_pair.rs:13`, `pub(crate) const DEFAULT_SAMPLE_RATE: f32 = 48000.0`).
This value drives the real prewarm computation for LSTM (24000 samples at 48 kHz) and the
sample-rate-dependent logic in `DSP::Reset()` for all architectures.

**Rationale:** the 48000 Hz default produces a correct, functional prewarm rather than the
C++ sentinel's near-zero prewarm (1 sample). All known production `.nam` models include
`sample_rate` explicitly, so the default is only exercised by degenerate or hand-crafted
models. In those cases, NAM-rs's behavior is measurably "more correct" — the model settles
to its steady state — while C++'s sentinel produces effectively no prewarm at all.

**Divergence assessment:** This is an **intentional, documented, low-risk divergence**.
It does not affect any known production model (every real community `.nam` export includes
`sample_rate`). The behavior affects only the degenerate zero-`sample_rate` case, where
NAM-rs's prewarm is strictly superior. The C++ sentinel is not emulated, and emulating it
has no practical benefit for any real-world use case.

**Verification:** this policy is enforced at two levels:

1. **JSON parse:** `validate_sample_rate` (`src/loader/nam_json/validation.rs`) rejects
   non-finite or ≤0 sample rates via `JsonError::InvalidSampleRate`, but `None` (absent
   field) passes through silently — it is handled downstream.
2. **Model build:** `build.rs:161` applies `unwrap_or(DEFAULT_SAMPLE_RATE)` to the parsed
   `Option<f32>`. LSTM dispatchers (`static_builder.rs:27,72`, `dynamic_builder.rs:32`)
   apply the same default independently for prewarm computation.

### 5.2 FastLUTActivation — Not Ported (F-P4-c)

**Background:** C++ NAMcore ships an optional `FastLUTActivation` class
(`NAM/activations.h:127-169`) that precomputes look-up tables for `tanh` and `sigmoid`
to accelerate inference on systems without fast `expf` hardware. It is controlled by
`Activation::enable_fast_tanh()` and `Activation::using_fast_tanh`.

**Status in NAM-rs:** `FastLUTActivation` is **not ported** and has no NAM-rs equivalent.
This is **not a parity gap** for the following reasons:

- `FastLUTActivation` is a **runtime optimization**, not a format/algorithm feature.
  The `.nam` file format has no field for "use lookup tables" — it is a local C++-side
  accelerator that produces the same mathematical output (within LUT precision) as exact
  `tanh`/`sigmoid` for identical weights.
- The NAMcore `render` tool (used for golden generation and live cross-validation) **never**
  enables it: `enable_fast_tanh()` is only called from benchmarking tools
  (`tools/benchmodel*.cpp`), not from `render.cpp`. This is confirmed in the NAMcore
  audited source (`activations.h:14`: `static bool using_fast_tanh = false` is the only
  initialization, and only `benchmodel*.cpp` flips it — verified by grep of all callers).
- NAM-rs's `ActivationPrecision::Standard` (universal default) already produces exact-grade
  `tanh`/`sigmoid` within 2×10⁻⁷ of C++'s exact math, making the LUT precision tradeoff
  irrelevant.

**Verdict:** FastLUTActivation is classified as **"Not Applicable"** — no port needed, no
parity gap, no audio divergence. Documented here for completeness and to prevent future
audit cycles from re-discovering and re-investigating it.

## 6. Other Architectures (Out of Scope)

ConvNet, Linear, `SlimmableContainer`, and the IR Cabsim convolution stage complete the model suite:

- **ConvNet — Paridade Total de Inicialização e Aritmética (✅ resolved 2026-07-28).** O vendored
  NAMcore implementa ConvNet (`NAM/convnet.cpp`), mas usando um formato flat com BatchNorm params
  brutos. O NAM-rs usa um formato nested por-bloco com BatchNorm pré-fundido scale/offset
  (`src/loader/dispatcher/convnet/mod.rs`). A divergência de ESR `2.54e-5` (SNR `45.9 dB`)
  previamente reportada era **exclusivamente um transiente de inicialização de estado (prewarm)**
  confinado às primeiras 62 amostras — o `ConvNetModel::prewarm()` preenchia zeros literais por
  bloco isolado, enquanto o NAMcore (`dsp.cpp:67-96`) processa `receptive_field_size + 1` amostras
  de silêncio através da rede inteira. A correção (`TASK-CONVNET-01`, 2026-07-28) replicou a
  semântica exata do NAMcore, eliminando o transiente.

  - **Métricas de paridade definitivas (pós-fix, 2026-07-28):**
    - **C++ cross-validation** (`quick_parity_convnet`): ESR = `4.20e-15` (SNR `143.8 dB`), MR-STFT = `1.20e-6`
    - **Oráculo f64** (`test_oracle_convnet`): ESR = `3.57e-15` (SNR `144.5 dB`, piso f32)
    - **Oráculo vs NumPy f64** (`test_oracle_vs_python_anchor_convnet`): ESR = `5.23e-33` (bit-exact)
    - **Self-golden** (`test_golden_vectors_convnet_test`): ESR = `0.00e0` (determinismo total)
  - **Gates de qualidade recalibrados:** SNR ≥ `120 dB`, ESR ≤ `1.0e-12`, MR-STFT ≤ `1.0e-4`
    (`TASK-CONVNET-05`).
  - **Teste de invariante:** `test_convnet_prewarm_fixed_point_invariant()` confirma que o
    estado pós-prewarm é um ponto fixo estacionário idêntico à convergência explícita (`TASK-CONVNET-03`).
  - CPU latency = `10.3 µs` (0.8% of RT budget) — inalterada (prewarm opera em caminho frio).

- **Linear (RF=2048 / 4096 / 8192).** Affine linear model. Baseline ESR vs NAMcore = `1.70e-14` (SNR `137.7 dB`), CPU latency = `0.3 µs` (0.0% of RT budget).

- **`SlimmableContainer`.** Multi-model crossfade orchestration, implemented and tested (`src/models/container.rs`, `tests/models/container_slimmable.rs`). Baseline A2 Example (CH=3→6) cross-validation vs NAMcore: ESR = `7.28e-14` (SNR `131.4 dB`), ESR vs f64 = `1.82e-14`, MR-STFT = `1.73e-05`.

- **IR Cabsim.** Impulse response convolution stage, cross-validated via `tests/parity/cabsim_cpp_parity.rs`.

---

## 7. Known-Broken Ledger ("Sabidamente Broken")

Single-page triage. Everything in this document up to here is evidence; this chapter is the
verdict. Read this chapter alone if the only question is *"what's safe to ship, and what isn't."*
Severity tiers are ordered by how much they should worry a release decision, not by section order.

### 7.1 🔴 Broken today — confirmed wrong audio output

Exactly **one** model, in the entire audited scope, produces confirmed-wrong output:

| Model                                                                    | Symptom                                                                                                                                                                                                                                                                                               | Status                                                                                                                                                                                                                                |
|:------------------------------------------------------------------------ |:----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |:------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| `wavenet_a2_max.nam` (WaveNet A2, official flagship, `condition_size=8`) | MSE≈2.46e3, SNR≈−15.6 dB, ESR≈3.61e1, MR-STFT≈3.41 vs. the C++ golden — every threshold missed by 3+ orders of magnitude. **Post-B1/B2/B3 (T3.9, 2026-07-10):** MSE≈7.30e3, SNR≈−20.3 dB, ESR≈1.07e2 — the FiLM corrections removed an accidental partial cancellation, exposing the full divergence. | Root cause **identified**: three confirmed production bugs in the A2 dynamic engine (head1x1 per-layer vs per-array; `groups` ignored in `layer1x1`/`input_mixin`; legacy head kernel K=1 vs hardcoded 16). Fix plan detailed in §4.4 |

No other model, in any of the three audited architectures, has a confirmed output-correctness
failure. This is the only item that should block a release if `wavenet_a2_max.nam` compatibility
is a requirement.

> **Mitigado/contido (2026-07-02):** desativado fail-closed na camada de dispatch
> (`is_disabled_broken_a2_flagship` em `src/loader/dispatcher/wavenet/mod.rs`,
> ver §4.4). `build_model` rejeita o modelo com `Err` antes de
> tocar pesos ou construir o motor. O modelo `.nam` e o golden `.bin` permanecem
> no repositório (disabled, not removed). Reativação depende de fechar a
> divergência do `condition_dsp` contra o golden C++ (§4.4).
>
> **Predicado de detecção (assinatura estrutural estreita):**
>
> ```text
> num_arrays == 1
>   && data.config.condition_dsp.is_some()
>   && l0.condition_size.unwrap_or(1) == 8
> ```
>
> A guarda casa **somente** o flagship quebrado (`single-array`, `condition_dsp`
> presente, `condition_size=8`). `wavenet_condition_dsp.nam` (multi-array,
> `condition_size=3`) e todos os fixtures FiLM/gated/blended/full/lite não casam
> — preservados, com golden vectors comprovadamente passando no `cargo test`.
>
> **Mensagem de erro ao usuário:**
>
> ```text
> WaveNet A2 flagship (single-array, condition_dsp, condition_size=8) is disabled:
> confirmed wrong audio output vs NAMcore golden — see docs/cpp_parity_map.md §7.1.
> Model is not removed; re-enable requires closing the condition_dsp parity gap (§4.4).
> ```
>
> **Impacto nos testes:** nenhum teste em `cargo test` (debug ou release)
> executa inferência de `wavenet_a2_max.nam`. Inventário de mitigação do modelo flagship:
>
> - `test_loader_gap_wavenet_a2_max` renomeado para
>   `test_wavenet_a2_max_dispatch_is_disabled_broken` — assera `Err` com a
>   mensagem acima (prova vivo de que a guarda está ativa).
> - `test_golden_vectors_wavenet_a2_max` mantido `#[ignore]` com razão citando
>   §7.1 e bloqueio no dispatch.
> - `test_oracle_vs_python_anchor_a2_generic` → `#[ignore]` com rastreamento
>   FU-1 (restauração do oráculo f64, bloqueado por §4.4).
> - `test_oracle_a2_generic`, `test_decomposition_a2_generic`,
>   `test_combined_simulation_a2_generic` → `#[ignore]` com razões atualizadas
>   para §7.1.
> - Meta-teste `threshold_calibration`: `"wavenet_a2_max"` removido da lista;
>   braço `validation.rs` mantido como morto documentado.
>
> **Invariante garantido:** `grep -rn 'wavenet_a2_max' src/ tests/` mostra
> apenas a guarda de desativação, o teste de asserção positiva,
> `#[ignore]`'s rastreados, e referências documentais.
>
> **Cobertura de regressão:** `test_wavenet_condition_dsp_still_loads` prova que
> a guarda não bloqueia modelos vizinhos válidos. `utils/tests-quick.sh` 100% verde.

### 7.2 🟡 Known, measured, accepted tradeoffs — not bugs

Every item below is a deliberate engineering tradeoff with a calibrated, tested error budget. They
show up as nonzero numbers in the tables throughout this document, but they are not defects:

- **LSTM backbone weight representation** — Storage uses native `f32` weights across all gate matrices, matching NAMcore's `Eigen::MatrixXf`. Eliminating weight quantization simplified GEMV kernel dispatch and improved per-sample latency by 10-12%, while bringing `BossLSTM-2x8` to bit-exact convergence with NAMcore (ESR = 1.00e-11).
- **`ActivationPrecision::Fast`'s Padé/minimax activation approximations** vs. C++'s exact
  `tanh`/`sigmoid` — small, bounded, and identical in nature for LSTM and WaveNet A1/A2 (§2.5,
  §3.2, §5). `Standard` (exact-grade, universal default) collapsed this gap to match C++ parity
  within measurement noise.
- **A2 FiLM dynamic-engine interop gap** — previously identified as an interop gap of SNR 18.1–36.0 dB,
  this was shown to be caused by a zero-biased initialization in the synthetic weight generator.
  Following the fix to apply standard identity-biased weights, the gap collapsed to float32 precision
  limits (SNR 138+ dB / ESR ~1e-14), achieving near-bit-exact parity (§4.3).

### 7.3 🟠 Test-infrastructure caveats — parity coverage that can silently vanish

These do not produce wrong audio, but they can make the *evidence* for parity evaporate without failing CI:

- **Silent SKIP in v1 live cross-validation.** `tests/parity/cpp_parity.rs::run_v1`
  now delegates to `run_v1_hf` (Standard/exact activation is the universal default),
  so v1 parity metrics are HF-mode. Both still discard `ParityOutcome`
  (`let _ = run_render_comparison(...)`). Any skip condition — C++ toolchain absent,
  model missing, render crash — prints `SKIP:` to stderr and the test still reports `ok`.
  The non-ignored `quick_parity_*` subset in `tests-quick.sh` Fase 2 can therefore pass
  green with **zero** cross-validations executed. (`run_v2_multi_sr_impl` tracks
  outcomes correctly and asserts the completed-rate set.)
- **`quick_parity_convnet`** previously always skipped (§6 — architecture incompatibility), but
  after the prewarm fix (TASK-CONVNET-01, 2026-07-28) it now passes with ESR=4.20e-15 (SNR 143.8 dB),
  completing the 4-model quick-parity matrix at full coverage.
- **`wavenet_a2_film_input_mixin_pre.nam`** has been fully validated with committed C++ goldens and live cross-validation (`live_cross_validation_wavenet_a2_film_input_mixin_pre`), achieving ESR `3.44e-14` (SNR `134.6 dB`, MR-STFT `6.92e-06`) against NAMcore with calibrated gates (SNR ≥ `120.0 dB`, ESR ≤ `1.0e-11`, MR-STFT ≤ `1.0e-4`).

---

## See Also

- [audio_fidelity_map.md](audio_fidelity_map.md) — off-spec DSP factors; §3 (LSTM recurrent drift) pairs with §2.5/§2.7 here
- [perceptual_validation.md](perceptual_validation.md) — metrics and gate-calibration policy
- `TODO-wavenet_a2_max.md` — live status and fix plan for the open A2 flagship investigation (§4.4)
- `tests/parity/cpp_parity.rs` — live cross-validation against the C++ `render` tool
- `tests/parity/reference_oracle_f64.rs` — f64 oracle and independent NumPy anchor (decomposition tools, §1.2)