# `v2.0.0-rc.2` TODO — what's left after rc.1

**Status snapshot (2026-05-10, end-of-day, second pass):** **All §1, §2,
and §3 items below are now closed in-tree.** The original §2 items were
deferred to a 2.0.x patch series; the second-pass session collapsed the
deferred queue and closed them in rc.2 directly. The §3 SKILL.md set is
also fully written.

The only remaining gap before tagging `v2.0.0` stable is the soak window
(see `docs/BENCH_BASELINE.md` for the criterion baseline workflow that
runs alongside the soak).

For closure details by section see the `project_v2_release_status`
memory entry and the `[2.0.0-rc.2]` CHANGELOG entry.

---

## Original document (preserved for traceability)

`v2.0.0-rc.1` shipped with all 6 P0 + 13 P1 + 23 P2 from the 2026-05-07
audit closed, **plus** all 11 P0 silent-correctness bugs + 3 P1 must-ship
items from the 2026-05-10 4-agent deep quant audit closed. This document
originally collected every remaining task we knew about that should land
before `v2.0.0` stable, plus the items we'd explicitly chosen to defer to
the `2.0.x` patch series.

**Source audits:**
- `docs/V2_RELEASE_AUDIT_2026-05-07.md` (rc.0 → rc.1 closure)
- `docs/QUANT_AUDIT_2026-05-10_pricing.md` (Agent A, ~25k LOC)
- `docs/QUANT_AUDIT_2026-05-10_riskvol.md` (Agent B, ~6.7k LOC)
- `docs/QUANT_AUDIT_2026-05-10_rates.md` (Agent C, ~13.3k LOC)
- `docs/QUANT_AUDIT_2026-05-10_misc.md` (Agent D, ~7k LOC)

Items here are **not** ranked by severity (no P0s remain — those all closed
in rc.1). Sectioning is by area: should-fix-before-stable vs deferred-to-patch.

---

## §1 Should-fix-before-`v2.0.0` stable (rc.2 candidates)

These items have meaningful user impact but are **not** silent-correctness
bugs. Most are quality-of-life, ergonomics, or post-stable-breaking changes
we'd rather make pre-stable than absorb a major bump for.

### §1.1 Pricing & calibration (Agent A residual P1s)

#### §1.1.1 Heston "Little Trap" form across the entire Heston family
- **Files:** `pricing/heston.rs:241-243, 251-258`,
  `pricing/fourier.rs:300-305, 343-345, 401-407, 643-645, 728-730`,
  `calibration/svj.rs:286, 288`, `calibration/double_heston.rs:289, 290`
- **Issue:** All Heston-family characteristic functions (`HestonPricer`,
  `HestonFourier`, `BatesFourier`, `HKDEFourier`, `DoubleHestonFourier`,
  SVJ calibrator) use the original Heston (1993) `(1−g·e^{−dT})/(1−g)`
  form, which has known principal-branch logarithm discontinuities for
  long maturities (T > 2y) and |ρ| close to 1. Albrecher, Mayer,
  Schoutens & Tistaert (2007) — "The Little Heston Trap" — show that the
  swap `g̃ = 1/g` form keeps the integrand on the principal branch for
  all T.
- **Action:** switch every Heston-family ChF to the Trap form. Add a
  regression test on a long-maturity high-|ρ| case (T = 5y, ρ = -0.9)
  verifying the original form fails and the Trap form succeeds.
- **Risk:** non-trivial. Touches 7+ files. SVJ calibrator computes its
  own ChF (doesn't delegate to BatesFourier), so the fix needs to be
  applied there too.

#### §1.1.2 Reachable `expect()` panics in user-facing constructors
- **Files / lines:**
  - `calibration/rbergomi.rs:471` — `params.validate().expect("Invalid initial rBergomi params")`
  - `calibration/rbergomi.rs:473` — `slice.validate().expect("Invalid market slice")` (loop on user-supplied slices)
  - `pricing/malliavin_thalmaier/heston.rs:148` — Cholesky on user `cross_corr` matrix
  - `pricing/malliavin_thalmaier/heston.rs:257` — Inverse of Malliavin covariance (singular for low-vol paths)
  - `pricing/basket.rs:245`, `pricing/rainbow.rs:263` — same Cholesky pattern
  - `pricing/dupire.rs:219, 223, 227, 344, 348, 352` — `expect("dc_dk must be provided …")` on `Option` fields
  - `pricing/breeden_litzenberger.rs:83` — same Dupire pattern
- **Action:** convert user-input constructors to return `Result<Self, Error>`,
  or document explicit pre-validation requirements. The Dupire `Option`
  pattern should be rewritten as a separate `Custom` variant so the type
  system enforces the invariant.

#### §1.1.3 `&dyn FourierModelExt` → generic dispatch
- **Files:** `pricing/fourier.rs:87, 141, 158, 177, 226`
- **Issue:** Carr-Madan / Lewis / Gil-Pelaez pricers take `&dyn
  FourierModelExt` for model-agnosticism, but the blanket
  `impl<T: FourierModelExt> ModelPricer for T` (`:251-255`) shows generic
  dispatch works here. CLAUDE.md / dev-rules forbid `&dyn` / `Box<dyn>`
  where concrete types work.
- **Action:** replace with `model: &impl FourierModelExt`. Carr-Madan FFT
  inner loop benefits from inlining vs vtable dispatch.

#### §1.1.4 Carr-Madan grid centring (Lord & Kahl 2010)
- **File:** `pricing/fourier.rs:96`
- **Issue:** `b = -π/η` symmetric centring around 0. For models with
  strong skew (HKDE, Bates with negative jump bias), the call grid centre
  is at K = S regardless of model. Cumulant-based grid centring (Lord &
  Kahl 2010) is the production standard.
- **Action:** centre the FFT grid on the model's cumulant-based optimal
  damping point, with the symmetric form as a fallback.

#### §1.1.5 Stray `scilib` dependency
- **File:** `pricing/cgmysv/model.rs:11`
- **Issue:** uses `scilib::math::basic::gamma`; the rest of the crate
  uses `stochastic_rs_distributions::special::gamma`. `scilib` is otherwise
  unused in `stochastic-rs-quant`.
- **Action:** unify on `stochastic_rs_distributions::special::gamma` and
  drop the dependency.

#### §1.1.6 `fourier_malliavin/engine.rs` empty-input panic
- **File:** `fourier_malliavin/engine.rs:45`
- **Issue:** `let n = prices.len() - 1;` underflows on empty input.
- **Action:** validate at the start of `new` / `new_uniform` /
  `with_freq` constructors.

### §1.2 Vol surface, risk, portfolio (Agent B residual P1s)

#### §1.2.1 SABR FX-smile hardcoded strike bounds
- **File:** `vol_surface/sabr_smile.rs:309-310`
- **Issue:** `lo = [1.0, 1.0, 1.0, 1.0, 0.01, -0.99]` and `hi = [10.0,
  10.0, 10.0, 10.0, 10.0, 0.99]` are bounds on `K_{rr,c}, K_{rr,p}, K_{bf,c},
  K_{bf,p}` chosen for BRL/USD-style FX. Fails for any underlying outside
  `[1.0, 10.0]` (equities, indices, penny FX, commodities).
- **Action:** scale bounds by spot (`s * 0.5` to `s * 2.0`) or expose
  bounds as struct fields with a `with_strike_bounds(...)` builder.
  Tighten `success` predicate (currently `f_best.is_finite()`) to use
  the same threshold the test enforces (`< 1e-3`).

#### §1.2.2 Optimizers hardcode `252.0` daily annualization
- **Files:** `portfolio/optimizers.rs:132, 339, 439`
- **Issue:** `risk/performance.rs` lines 11-13 explicitly state "no 252,
  365, or 12 constants are hard-coded". `portfolio_vol_from_returns` and
  `empirical_cvar` callers in CVaR objective break this rule. Users with
  weekly / monthly / hourly returns silently get wrong annualized vol.
- **Action:** thread `periods_per_year` through `optimize_with_method`
  into `portfolio_vol_from_returns` / `empirical_cvar`. Default 252 for
  backward compat.

#### §1.2.3 Hardcoded `penalty = 10.0` in mean-variance / mean-CVaR objectives
- **Files:** `portfolio/optimizers.rs:173, 257, 353, 453`
- **Issue:** `port_var + penalty * (port_ret - target_return)^2` with
  `penalty` fixed at 10. Reasonable for daily portfolios, swamps risk
  for weekly returns.
- **Action:** add `lambda: f64` parameter to optimizer signatures; default 10.

#### §1.2.4 `align_return_series` silent date-misalign
- **File:** `portfolio/data.rs:64-75`
- **Issue:** right-aligns the last `min_len` observations of each series.
  Silently assumes all input series end on the same date — if asset A
  ends on 2026-05-10 and B ends on 2026-04-30, A's last 10 days get
  matched against B's 2026-04-21..04-30 (date-misaligned).
- **Action:** require dates as part of API and align by date, **or**
  document the right-aligned assumption loudly, **or** accept
  `start_indices: &[usize]` for explicit caller control.

#### §1.2.5 `OptimizerMethod::from_str` silent Markowitz fallback
- **Files:** `portfolio/types.rs:26-39` + `portfolio/momentum.rs:109-118`
  (same anti-pattern for `WeightScheme`)
- **Issue:** `Err = Infallible`, `from_str("garbage")` returns `Ok(Markowitz)`.
- **Action:** define a real error type; reject unknown strings.

#### §1.2.6 SSVI `project()` doesn't enforce calendar bound
- **File:** `vol_surface/ssvi.rs:146-159`
- **Issue:** Only enforces the butterfly bound `η(1+|ρ|) ≤ 2`. After the
  rc.1 fix to `is_calendar_spread_free` (full GJ 2014 Theorem 4.2), the
  calendar condition can be violated even for parameters that pass
  `satisfies_no_butterfly_condition`.
- **Action:** extend `project()` to enforce both. The calendar bound after
  the power-law SSVI substitution becomes a constraint linking η, ρ, γ
  that's stricter than the butterfly bound.

#### §1.2.7 SSVI Jacobian via 1-sided forward differences
- **File:** `vol_surface/ssvi.rs:433-461`
- **Issue:** SSVI LM problem implements `jacobian` via forward differences
  with `h = 1e-7`. Analytic derivatives (`phi`, `phi_prime`, `w_prime_k`,
  `w_double_prime_k`, `w_prime_theta`) are all already implemented in
  the same file. Forward-diff has O(h) error vs analytic O(eps); strictly
  slower (3 extra evals per parameter per data point).
- **Action:** replace with the closed-form Jacobian using existing
  derivatives.

#### §1.2.8 `SmileSlice::atm_total_variance` returns endpoint when smile is one-sided
- **File:** `vol_surface/implied.rs:353-379`
- **Issue:** If a smile slice has only ITM/OTM strikes on one side of `k=0`,
  the function returns `total_variance[0]` or `total_variance[n-1]` —
  i.e., a non-ATM total variance — and the caller (`fit_ssvi`,
  `pipeline.rs:121`) treats it as the ATM input θ. Biases the global
  (ρ, η, γ) fit.
- **Action:** linear extrapolation to `k=0` from the two innermost data
  points; log a warning when a slice doesn't straddle zero.

### §1.3 Rates, lattice, instruments (Agent C residual P1s)

#### §1.3.1 G2++ joint-probability symmetric-branch limitation
- **File:** `lattice/short_rate.rs:654-710`
  (`correlated_joint_probabilities`)
- **Issue:** `λ = ρ/12` corner correction is exact for the symmetric
  trinomial branch (`p_u = p_d = 1/6, p_m = 2/3`). For drift-shifted
  branches (`p_u ≠ p_d`), the correction doesn't hit the requested
  factor covariance. Bias proportional to drift; ~10⁻³ on joint
  probabilities for typical `a ≤ 0.05`, `dt ≤ 0.25`; multi-year products
  pick up a few-bps bias.
- **Action:** either replace `λ = ρ/12` with the per-cell covariance
  solver from Hull-White (2000) eq. 16-18, **or** tighten the docstring
  to call out the symmetric-branch-only exact-matching property and
  mark this as Tier-0 / sketch-grade.

#### §1.3.2 Bootstrap swap formula off-by-one for non-integer maturity
- **File:** `curves/bootstrap.rs:75-98`
- **Issue:** For a 1.4Y semi-annual swap (`maturity*frequency = 2.8`),
  `n_payments = round(2.8) = 3` mismatches the swap-quote convention.
  Bootstrap silently gives wrong DFs.
- **Action:** assert `maturity * frequency` is integer, **or** route
  swap-bootstrap exclusively through the date-aware
  `market::rate_helper::SwapRateHelper` and gate the free-form
  `Instrument::Swap` path as "approximate".

#### §1.3.3 Calendar helper duplication across 3 modules
- **File:** `cashflows/types.rs:393-439`
- **Issue:** `add_months`, `days_in_month`, `is_leap_year` exist in three
  copies (`calendar/schedule.rs:227`, `calendar/day_count.rs:126,130`,
  `cashflows/types.rs`). All production paths.
- **Action:** lift one canonical impl to `crate::calendar::date_math`
  (or `pub(crate) use` from `calendar::day_count`), delete the duplicates.

#### §1.3.4 Inflation YoY without convexity adjustment
- **File:** `inflation/swap.rs:97-113`
- **Issue:** `YearOnYearInflationSwap::inflation_leg_pv` builds
  `E[I(T_i)/I(T_{i-1})]` from deterministic ZC forward ratios, ignoring
  the convexity term that depends on joint nominal/real volatility
  (Mercurio 2005 §6 / Brigo-Mercurio §16.5). Test passes only because
  the model is deterministic; convexity bias appears under non-trivial
  vol.
- **Action:** add explicit "deterministic-curve assumption" docstring on
  `inflation_leg_pv` and link to the Mercurio (2005) §6 convexity formula
  for callers who need it.

#### §1.3.5 Bond YTM solver `panic!` on bracket failure
- **File:** `instruments/bond/shared.rs:160, 377, 388`
- **Issue:** `yield_to_maturity_from_dirty_price_for_leg` (and
  OAS / Z-spread solver) bracket the yield, expand twice, then `panic!`
  if it can't bracket. Pathological inputs (deeply distressed bonds,
  dirty price > all coupons) kill the pricing process.
  `find_root_brent(...).expect(...)` at `:171`.
- **Action:** return `Result<T, BondPricingError>` or saturate to the
  extrapolated boundary with logged warning. At minimum replace `expect`
  with documented fallback.

#### §1.3.6 Monotone-convex interpolation has unused locals + missing corner cases
- **File:** `curves/interpolation.rs:234-245`
- **Issue:** `g` and `integral` locals computed and immediately discarded
  via `let _ = ...`. The actually-returned `forward_adj` (`:247`)
  re-derives a polynomial integral but skips the Hagan-West (2006)
  corner cases (eqs 25-27). Looks rushed.
- **Action:** delete unused locals and rename the function to reflect
  Tier-1 status, **or** finish the implementation per Hagan-West.

### §1.4 Microstructure, traits, factors (Agent D residual P1s)

#### §1.4.1 `yahoo.rs` not production-ready
- **File:** `stochastic-rs-quant/src/yahoo.rs`
- **Issues:**
  - 14 `.unwrap()` calls on the public surface (lines 57, 88-92, 94, 96,
    104, 114-116, 140, 157, 168, 172, 183, 188-189, 193-198, 210). Network
    or IO failure crashes the process.
  - 3 online tests hit live AAPL on every `cargo test --features yahoo`
    run. CI flakes on rate limits / weekends / listing changes / network
    outages.
  - `tokio_test::block_on` (lines 87, 111) used in production code (intended
    for unit tests).
  - `ReturnType::Absolute` (lines 169-183) computes `price[t] / price[t-1]`
    (gross return), neither absolute nor a return.
  - `Default` panics on `YahooConnector::new().unwrap()` (line 57).
- **Action — pick one:**
  - (a) Mark `feature = "yahoo"` experimental in README; replace `unwrap()`
    with `Result`-returning API; gate live tests behind
    `feature = "yahoo-online"`; rename `ReturnType::Absolute` to
    `GrossReturn` (or fix to `price[t] - price[t-1]`).
  - (b) Remove `yahoo.rs` from `v2.0.0` stable and re-introduce as
    a `stochastic-rs-yahoo` micro-crate in 2.x.

#### §1.4.2 Order book exact-equality fp comparison
- **File:** `order_book.rs:98, 137, 196, 255`
- **Issue:** `if maker.size == 0.0 { ... }` after `maker.size -= traded`.
  Sequential partial fills can leave `maker.size = 1e-17` and the maker
  stays in the book forever.
- **Action:** use `maker.size <= EPSILON` (or — better — track sizes as
  `u64` minor units, the typical exchange convention).

#### §1.4.3 Order book `expect("time went backwards")` on add_order
- **File:** `order_book.rs:165-168`
- **Issue:** `SystemTime::now().duration_since(UNIX_EPOCH).expect(...)`.
  Realistic during NTP step-back.
- **Action:** `unwrap_or(Duration::ZERO)` — orders submitted during clock
  anomalies still get unique ids via `next_id`.

#### §1.4.4 `factors/{pca,fama_macbeth,pairs}.rs` `.expect()` panics
- **Files:** `factors/pca.rs:52-54` (`svd().expect("SVD failed")`),
  `factors/fama_macbeth.rs:55, 70` (`least_squares(&y).expect(...)` ×2),
  `factors/pairs.rs:91` (`.expect("hedge ratio OLS failed")`)
- **Issue:** Public functions can fail on rank-deficient inputs (pure
  noise, collinear columns).
- **Action:** return `Result<…, FactorsError>`.

#### §1.4.5 Roll spread denominator off-by-one
- **File:** `microstructure/spread.rs:36-44`
- **Issue:** `cov` divided by `count = diffs.len() - 1` (biased
  autocovariance with `n-1` divisor). Roll 1984 uses `n`. Both
  defensible; docstring says nothing.
- **Action:** align with whichever convention the docstring and reference
  agree on.

#### §1.4.6 Almgren-Chriss documentation/implementation mismatch
- **File:** `microstructure.rs:8-9` (parent module doc) vs
  `almgren_chriss.rs:13-15` (implementation)
- **Issue:** Parent doc shows `κ = √(λσ²/η̃)` (continuous-time limit);
  implementation correctly uses `κ = (1/τ)·arccosh(1 + λσ²τ²/(2η̃))`
  (discrete). They agree only as `τ → 0`.
- **Action:** update parent doc to match the discrete form, **or** label
  it explicitly as the continuous limit.

#### §1.4.7 Almgren-Chriss `Buy` only flips `rates`, not `inventory`/`trades`
- **File:** `almgren_chriss.rs:192-196`
- **Issue:** Caller code using `plan.inventory` or `plan.trades` for a
  buy sees "sell-shaped" numbers.
- **Action:** flip both, **or** document the sign convention prominently
  and rename `trades`/`inventory` to make the sell-frame explicit.

#### §1.4.8 Migration matrix `power(0)` undefined
- **File:** `credit/migration.rs:69-76`
- **Issue:** `assert!(k >= 1)` panics on `k = 0`, but `P^0 = I`.
- **Action:** accept `k = 0` (return identity) **or** document the
  convention.

#### §1.4.9 `lib.rs` flat module list
- **File:** `stochastic-rs-quant/src/lib.rs:26-59`
- **Issue:** 23 public modules listed flat. `cargo doc` produces a flat
  list. The current audit-level commentary at lines 32-54
  (PCA / fourier-malliavin / microstructure / order_book) helps; extend
  to `portfolio`, `strategies`, `inflation`, `lattice`, `fx`.
- **Action:** add doc-comment subcategories.

#### §1.4.10 `python.rs` 63 kB monolith
- **File:** `stochastic-rs-quant/src/python.rs` (2617 lines)
- **Issue:** PyO3 bindings work but unwieldy; refactor into
  `python/{pricing,calibration,vol_surface}.rs` for navigability.
- **Action:** split. Not a blocker; may slip to 2.0.x.

### §1.5 Python bindings — must update for rc.1 API changes

The rc.1 fix-pass changed several public APIs. Python wrappers in
`stochastic-rs-py` need matching updates **before** stable so Python users
don't get a worse API than the Rust users.

- **`HscmModel::price_call`** now uses `q` (was `_q`). Python wrapper
  needs to pass `q` correctly. New `HscmCalibrator` struct should expose
  the standard `Calibrator` Python wrapper.
- **`RBergomiCalibrator::with_dividend_yield(q)`** builder added — Python
  wrapper needs the `dividend_yield` keyword/property.
- **`NigFourier`** new pricer struct — needs `PyNigFourier` wrapper
  registered in `stochastic-rs-py/src/lib.rs`.
- **`HullWhite::from_curve(...)`** new builder — Python wrapper needs to
  accept a `DiscountCurve` argument (the `fn(f64)->f64` field that was
  the Python-binding blocker is gone).
- **`multi_period_kyle`** output values changed (canonical Cetin-Larsen
  recursion). Any Python tests asserting the old (β·λ = 0.25 at terminal)
  values must be updated to 0.5.
- **`CarrMadanPricer::price_call`** now returns NaN for out-of-grid
  strikes (was 0.0). Python users must know to detect via `math.isnan()`.
  Expose `strike_in_grid()` as a Python method.
- **`SsviSurface::is_calendar_spread_free`** signature change — now takes
  a `ks: &[T]`. Python wrapper needs the new k-grid argument; old
  no-arg form maps to `is_atm_calendar_spread_free`.
- **`empirical_cvar`** assertion (`alpha < 0.5`) — Python users get
  `RuntimeError` (or panic-translated `pyo3::PanicException`) on
  confidence-style misuse. Document explicitly in pydoc.
- **`CgmysvCalibrator` / `HKDECalibrator` / `HscmCalibrator`** now
  implement the unified `Calibrator` trait — Python wrappers should
  expose `.calibrate()` returning a `Result`-style `Optional[Output]`
  or raising on failure (consistent with the existing `BSMCalibrator`,
  `HestonCalibrator`, etc. pattern).
- **`SabrParams`** `From<DVector>` now requires a 4-vec `[α, β, ν, ρ]`
  (was 3-vec). Python users constructing `SabrParams` from numpy arrays
  must include β. Update pydoc and any examples.

### §1.6 Stable cut criteria

- [ ] All §1.1-§1.5 items above closed.
- [ ] `cargo test --workspace --all-features`: 100% (one pre-existing
      RNG-sensitive flake in
      `stats::fukasawa_hurst::estimate_h_from_simulated_rv` should be
      pinned with a deterministic seed before stable to make CI happy).
- [ ] `cargo clippy --workspace --all-targets -- -D warnings`: clean.
- [ ] `cargo bench` baseline established and tracked vs rc.1
      (no regressions > 5%).
- [ ] Migration doc (`docs/V1_TO_V2.md`) updated with the rc.1 → stable
      breaking-change list:
      - `simulate_rbergomi_terminal_samples` signature gained a `q` parameter
      - `SsviSurface::is_calendar_spread_free` signature gained a `ks: &[T]`
        parameter
      - `bonds::HullWhite` rewritten — old `theta: fn(f64)->f64` and
        flat-curve fields are gone; users must now provide a
        `DiscountCurve` via `HullWhite::from_curve(...)`.
      - `CarrMadanPricer::price_call` now returns NaN for out-of-grid
        strikes (was 0.0).
      - `empirical_cvar` (and `cvar_alpha` field) asserts `alpha < 0.5`.
      - `SabrParams` `From<DVector>` now requires 4-vec.
      - `multi_period_kyle` output values changed.
      - `TimeExt::tau_or_from_dates` / `tau_with_dcc` now return NaN
        instead of panicking on missing data.
- [ ] Soak: ~3-7 days of regression runs + integrator feedback. Tag
      `v2.0.0` only after the soak window closes with no new P0/P1.

---

## §2 Deferred to `2.0.x` patch series (P2 polish)

Items below are non-blocking for stable but should land in the patch
series after `v2.0.0`. Grouped by audit slice.

### §2.1 Pricing & calibration (Agent A P2s)

- `pricing/heston.rs:223, 231` — `panic!("Invalid j")` is unreachable
  invariant; replace with `unreachable!()`.
- `pricing/sabr.rs:445` — hardcoded `30.0/365.0` in test only; OK but
  inconsistent.
- `pricing/cgmysv/pricer.rs:368, 389, 403, 429, 451` — hardcoded
  `28.0/365.0` and `25.0/365.0` in test code (matches paper's published
  maturities). Fine but worth a comment.
- `pricing/variance_swap.rs:88` — `.partial_cmp(&...).unwrap()` on f64
  ordering. NaN strikes panic. Edge case; document or filter NaN at input.
- `pricing/snell_envelope.rs:215` — same pattern.
- `calibration/hw_swaption.rs:243` — `coupon_times.last().unwrap()` after
  presumed non-empty check. Verify upstream invariant.
- `pricing/regime_switching.rs:30-86` — hand-written matrix multiply / add
  / inf-norm / scale. `nalgebra` does this with overloaded operators and
  is already a dependency. Refactor for readability.
- `pricing/heston_stoch_corr.rs:1` doc — says "Carr-Madan dampened FFT"
  but implementation uses double-exponential quadrature (line 239), not
  FFT. Mislabelled.
- `fourier_malliavin/engine.rs:117` — `m.unwrap_or((self.n_freq as f64).sqrt() as usize)`
  silent rounding; document `m ≈ √N` as Malliavin-Mancino canonical choice.
- `pricing/heston_stoch_corr.rs:25` — citation comment uses non-ASCII
  `Tanaś`. UTF-8 OK, but inconsistent with rest of codebase.

### §2.2 Vol surface, risk, portfolio (Agent B P2s)

- SVI `project()` (`svi.rs:108-119`) adds `|v_min|` to `a` to force
  `min_variance ≥ 0` but never re-checks butterfly density `g(k)` —
  corrected slice can still violate butterfly arb-freeness.
- `historical_es` (`risk/expected_shortfall.rs:73-93`) lacks the
  Rockafellar-Uryasev finite-sample tail-share correction. Approximate
  ES rather than the coherent estimator. Fine on continuous distributions;
  biased with ties.
- `running_drawdown` (`risk/drawdown.rs:22`) initializes `peak =
  T::min_positive_val()` (≈ 2.2e-308 for f64). Works for typical
  positive-equity series; nonsense if input series can go negative or
  starts below `min_positive_val`. Use `T::neg_infinity()`.
- `max_drawdown_duration` (`drawdown.rs:45`) measures the longest
  "consecutive bars below previous peak" — alternative "longest
  peak-to-recovery distance" not provided.
- `bucket_dv01` docstring (`risk/greeks.rs:70-72`) says "(V(up) -
  V(down)) / 2" but return is dollar PnL per `bump_size` move with sign
  opposite to the standard "DV01 is positive for long bonds" convention.
- Hardcoded Carr-Madan defaults `(N=2^12, α=0.75)` in
  `model_surface.rs:83 (fourier_model_surface_fft)`. Reasonable but
  should be settable.
- `sabr_smile.rs:269` — `MoreThuenteLineSearch::new().with_c(1e-4,
  0.9).unwrap()` inside basin-hopping loop. Wolfe coefficients are
  constants so this can't fail in practice; `.expect("Wolfe params
  (1e-4, 0.9) are valid")` would document why.
- `vol_surface/implied.rs:227` — `panic!("missing forward for tau={t}")`
  on input data path. Should be `Result`.
- `Sortino` denominator (`risk/performance.rs:73`) uses `n` (full
  sample) not `n_downside`. Bawa-Lindenberg vs Sortino-Price convention.
  Doc the choice.
- `portfolio/momentum.rs::assign_weights(WeightScheme::ScoreWeighted)`
  weights by `|score|` then normalizes — for short basket the user may
  expect short-side weights to also sum to 1; doc it.
- `portfolio/optimizers.rs::mat_inverse` (line 826-884) hand-rolled
  Gauss-Jordan inverse with no SPD/Cholesky path. For Black-Litterman
  100×100 covariance — `nalgebra::DMatrix::try_inverse` would be both
  faster (LU with partial pivoting) and more numerically stable.
- `Strategies` module is one struct (`DeltaHedge`, 108 LOC). Directory
  exists but no `Strategy` trait, no time-series interface, no backtest
  engine. `delta_hedge.rs::moneyness` has odd asymmetric thresholds
  (only DOTM tested below, only DITM above) — reasonable but worth
  documenting.

### §2.3 Rates, lattice, instruments (Agent C P2s)

- `bonds/cir.rs:13-29` field naming `theta`/`mu` swap matches the §1.3
  P0 fix completed in rc.1; if any downstream confusion remains, doc it.
- `fx/quoting.rs:88-105` — `cross_rate` returns `(pair, rate)` where the
  constructed pair's `(base, quote)` ordering depends on matched legs
  without enforcing market priority order. User who calls
  `cross_rate(usdjpy, ..., eurusd, ...)` gets a JPY-base pair instead of
  EUR-base. Doc warning.
- `market/observable.rs:65, 73, 81` — `expect("observable poisoned")` on
  `Mutex::lock`. Fine for in-process use; if this runs in a long-lived
  service the panic should be logged + observer evicted instead of
  crashing the process.
- `curves/types.rs:95` — struct named `Instrument` collides with
  `crate::instruments::*` namespace. Rename to `BootstrapInstrument` or
  move under `curves::bootstrap::Instrument`.
- `calendar/day_count.rs:16` missing variants: `Bus/252` (Brazil),
  `ACT/364`, `ACT/ACT AFB`, `NL/365` (no-leap), `30E/360 ISDA` variant.
  Each is a few-line addition. Affects fixed-income pricing in Brazil /
  Latin America.
- `market/rate_helper.rs:281, 316, 322` — test-only
  `NaiveDate::from_ymd_opt(...).unwrap()` are fine but inconsistent with
  prod paths' `?`-propagation.
- `fx/forward.rs:33-40` — `Display` impl uses `.to_f64().unwrap()`.
  For non-`f64` `T` this would panic on display. Replace with
  `T::to_f64().unwrap_or(f64::NAN)`.
- `lattice/short_rate.rs:584, 594` —
  `(((mean - x0) / dx).round().to_f64().unwrap()) as isize` panics on
  NaN (e.g. zero diffusion + zero drift). Defensive default to 0.
- `calendar/schedule.rs:155-181` — `ScheduleBuilder::build` doesn't
  expose stub conventions (long-first / short-first / long-last /
  short-last). Backward generation defaults to "short first" implicitly;
  forward defaults to "short last". ISDA 2006 §4.15 defines all four.

### §2.4 Microstructure, traits, factors (Agent D P2s)

- `factors/shrinkage.rs:146` — `let _ = Axis(1);` stray no-op.
- `credit/bootstrap.rs:151` — `let _ = k;` stray no-op (loop
  `enumerate()` could just be `for quote in &sorted`).
- `migration.rs:216` — `let _ = total_off;` stray no-op (local computed
  but unused).
- `loss.rs` — `mae`, `mre`, `mape`, `mspe`, `mpe` silently return `0.0`
  if `market_i ≈ 0` (lines 38, 53, 75, 91). For zero-strike instruments
  this masks bugs. Return NaN or filter inputs upstream.
- `traits/instrument.rs:9` — doc-example uses `r.greeks().unwrap().delta`.
  Fine in `///` rustdoc, but reads as encouraging `unwrap()` to consumers.
- `types.rs:118-120` — `CalibrationLossScore::get` returns `0.0` for
  missing metrics. Inconsistent with `f64::NAN` defaults elsewhere;
  switch to `Option<f64>` or `f64::NAN`.
- `microstructure/spread.rs:147-162` test
  `roll_recovers_known_spread_under_pure_bounce` — variance of the
  estimator is large for n=10000 (tolerance 0.03 on 0.10 spread).
  On a different RNG seed could be flaky; pinned seed=11 already
  helps but worth verifying.
- `factors/pca.rs:46` — `to_f64().unwrap()` on `T: FloatExt` values.
  `FloatExt` already has `Default + ToPrimitive`; `unwrap()` unreachable
  but noisy.
- Migration matrix `expm` regression test against
  `scipy.linalg.expm` — not a bug per se but recommended for
  scaling-and-squaring + Padé-13 implementation.

---

## §3 SKILL.md proposals (audit §9, 16 items)

Per the user directive ("először csináld meg az előző 1-6 taskokat amiket
felsoroltál a tier 1-2-3 majd utána ha azzal végeztünk"), these come
**after** rc.2 / stable. Two SKILLs already exist (`dev-rules`,
`new-module`) covering module-creation. The 16 below come from the 4-fork
audit consolidated by `docs/V2_RELEASE_AUDIT_2026-05-07.md` §9.

### §3.1 Tier 1 — release-level (highest leverage)

These four formalise the manual workflow we executed by hand for the rc.0
→ rc.1 → rc.2 cycle. Writing them turns "Claude knows the workflow" into
"the team can ship without Claude in the loop".

1. **`release-checklist`** — version bump on 9 crates (workspace.dependencies),
   `cargo test --workspace --exclude stochastic-rs-py --features openblas`,
   `cargo clippy --workspace --all-targets -- -D warnings`,
   `cargo check --workspace --all-features` (catches §4.1 type traps),
   `publish.sh --dry-run`, signed git tag, wheels build + PyPI publish
   (incl. macOS Intel), CHANGELOG + CLAUDE.md update,
   docs.rs feature lockdown verification.
2. **`feature-flag-management`** — `dep:` proxy syntax, propagation
   through workspace via `stochastic-rs-X/feature`, combinatorial
   explosion avoidance, `cargo check --features X,Y` matrix discipline.
   **Prevents the §4.1 trap.**
3. **`calibration-pattern`** — `Calibrator` impl pattern:
   `Result<Output, Error>` (anyhow), `CalibrationResult` (`type Params`,
   `iterations`, `message`, `max_error`, `loss_score`),
   optional `ToModel` / `ToShortRateModel` bridge. 13 `ToModel` impls +
   12 `Calibrator` impls (after rc.1's 3 new ones) as reference.
4. **`greeks-pattern`** — `GreeksExt` impl: which Greek when, `f64::NAN`
   defaults, **mandatory** `greeks()` override on MC pricers for
   single-pass consistency. `GbmMalliavinGreeks`, `HestonMalliavinGreeks`
   reference.

### §3.2 Tier 2 — domain-specific (frequent workflows)

5. **`add-diffusion-process`** — `ProcessExt<T>` impl, `pub fn new` /
   `pub fn seeded` parity, `py_process_1d!` macro, deprecated alias in
   `aliases.rs` for renames. **Explicitly** documents
   `theta`=mean-reversion-speed, `mu`=long-run-mean convention (prevents
   the §4.3 trap that shipped through rc.0).
6. **`add-fractional-process`** — when to extend `MarkovLift` vs wrap
   `Fgn`; when to add a new `Volterra` kernel. References Bilokon-Wong
   2026, Ma-Wu 2021, Abi Jaber 2018. **Forbids** the
   `sum_z * t.powf(H-1/2)` shortcut (the §5.10 trap).
7. **`add-jump-process`** — generic `D: Distribution<T> + Send + Sync`
   parameter, `CompoundPoisson` integration, characteristic-function
   consistency (Lévy-Khintchine), `(r and r_f)/b/mu` validation **at
   construction** (prevents the §5.5 17-panic class).
8. **`adding-distribution`** — `SimdXxx` pattern (A: transformation,
   B: ziggurat, C: rejection/inversion). Required: `new` + `with_seed` +
   `from_seed_source` + `fill_slice` + `fill_slice_fast` +
   `Distribution<T>` + `DistributionExt` override (closed-form, never
   `statrs`!) + KS test + `distribution_ext_vs_reference` entry +
   `py_distribution!` macro + LaTeX `//!` doc.
9. **`adding-python-binding`** — recipe for extending `stochastic-rs-py`:
   `py_distribution!` / `py_process_*!` invocation in producer crate,
   `IntoF32` / `IntoF64` shim verification,
   `m.add_class::<PyXxx>()` registration in
   `stochastic-rs-py/src/lib.rs`, `cd stochastic-rs-py && maturin develop`,
   smoke test (Python script or `Python::with_gil` block).
10. **`stats-estimator`** — `ArrayView1<T>` input, `*Result` struct
    (Debug+Clone+named fields), parametric vs bootstrap p-value,
    openblas-gating, paper citation header, **comparison test** against
    R/Python reference.

### §3.3 Tier 3 — niche (lower priority, less frequent)

11. **`copula-bivariate`** — `BivariateExt` impl: `theta_bounds`,
    `invalid_thetas`, **closed-form `compute_theta` or
    `roots::find_root_brent`** (never custom Newton — prevents the
    §6.12 Frank tau formula bug), `pdf` / `cdf` / `percent_point` /
    `partial_derivative`, parametric-tau recovery test.
12. **`add-gpu-sampler`** — pre-flight checklist (FFT length 2^k,
    Philox vs cuRAND, fused-kernel layout). `noise/fgn/cuda_native.rs`
    Fused Philox+scale NVRTC kernel as reference. `ProcessExt` 4
    `sample_*` overrides.
13. **`add-mc-variance-reduction`** — antithetic / control-variate /
    stratified / importance / quasi-MC (Halton/Sobol) / MLMC pattern
    guide. Glasserman 2003, Giles 2015. New estimator → `McEstimate<T>`
    return + 95% CI test.
14. **`vol-surrogate-nn`** — `StochVolModelSpec::new()` validation,
    BoundedScaler / StandardScaler convention, gzip-npy loader,
    train_save_load roundtrip test + real-trainset fit plot,
    `predict_surface` doc must point at
    `ImpliedVolSurface::from_flat_iv_grid`.
15. **`integration-test-writing`** — pinned seed mandatory, no debug
    `#[test]`, golden-numerics paper reference, feature-gating
    discipline (catches the §4.6 trap).
16. **`bench-writing`** — criterion harness + group naming + parameter
    sweep + `[[bench]] required-features` gate, no `println!`, no dead
    helper (catches the §6.1 drift).

### §3.4 Skipped (low value or not painful yet)

Per audit §9 "Skip" list — **don't** write these:

- `add-malliavin-greek` — 0 impls currently, deferred until actually needed.
- `add-rng-engine` — xoshiro is good enough, post-xoshiro far away.
- `writing-simd-ziggurat` — only `distributions` uses it; folds into
  `adding-distribution`.
- `special-functions-policy` — single file (`special.rs`), document in-place.
- `stationarity-test` — folds into `stats-estimator`.
- `mle-process` — folds into `stats-estimator`.
- `adding-plottable` — single file in `viz` crate, not worth a skill.
- `example-writing` — folds into `release-checklist` + `integration-test-writing`.
- `docs-rs-publication` — one-time `Cargo.toml` metadata, already correctly
  configured.
- `fgn-perf-cookbook` — single in-tree `noise/fgn/README.md` is enough.

---

## §4 Effort estimate

| Section | Items | Estimated effort |
|---|---|---|
| §1.1 Pricing & calibration P1s | 6 | ~2 days |
| §1.2 Vol/risk/portfolio P1s | 8 | ~2 days |
| §1.3 Rates/lattice/instruments P1s | 6 | ~2 days |
| §1.4 Microstructure/traits P1s | 10 | ~1.5 days |
| §1.5 Python bindings update | 10 sub-items | ~1 day |
| §1.6 Stable cut criteria (test, docs, bench, soak) | – | ~1 day code + 3-7 days soak |
| §2 P2 polish (deferred to 2.0.x) | ~30 items | post-stable |
| §3.1 Tier 1 SKILLs | 4 | ~1 day |
| §3.2 Tier 2 SKILLs | 6 | ~1.5 days |
| §3.3 Tier 3 SKILLs | 6 | ~1 day |
| **Total rc.1 → stable (excl. soak)** | – | **~10-12 focused days** |

The rc.1 → stable critical path is **§1.1-§1.6** (~9 focused days + soak).
Tier 1 SKILLs (§3.1) should land in parallel with the soak so the team
can ship `v2.0.1` patches without manual workflow reconstruction.

---

*Document drafted: 2026-05-10, end-of-day after rc.1 fix-pass closure.*
*Author: rc.1 fix-pass session — see `.claude/projects/.../memory/project_v2_release_status.md`*
*Source audits: `docs/V2_RELEASE_AUDIT_2026-05-07.md` + `docs/QUANT_AUDIT_2026-05-10_*.md`.*
