# Changelog

All notable changes to the ballistics-engine project will be documented in this file.

The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/),
and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).

## [0.22.8] - 2026-07-06

### Added

- **`estimate-bc` now estimates G1 *and* G7 BCs, and can fit velocity data as well as drop
  data.** Previously the command fit a single G1 BC from a drop curve only. New options:
  - `--drag-model g1|g7|both` (default `both`) — estimate the BC referenced to either
    standard drag model, or both at once. (A bullet's G7 BC is a different number from its G1
    BC — for a boat-tail, roughly half; both are now reported side by side.)
  - `--velocity-data "dist,vel;..."` — fit against a downrange velocity-retention curve
    instead of, or in addition to, drop. A velocity fit doesn't depend on the zero, sight
    height, or launch angle, so it is often the more reliable basis.
  - `--data "dist,drop;..."` on the native CLI too (n-point drop series), alongside the
    existing `--distance1/--drop1/--distance2/--drop2` input (now optional).

  The command prints one row per (drag model × supplied data basis), each with a fit-quality
  RMS. Supplying both a drop series and a velocity series with `--drag-model both` yields all
  four variants (G1/drop, G1/velocity, G7/drop, G7/velocity). Native CLI (table/json/csv) and
  WASM. New public API `estimate_bc_fit` / `BcFitMode` / `BcEstimate`; the existing
  `estimate_bc_from_trajectory` remains as a G1/drop convenience wrapper. Requested by an
  external WASM user.

## [0.22.7] - 2026-07-05

### Changed

- **Dependency updates.** Bumped core dependencies to their current major
  versions. No engine API or physics changes; the native binary, the WASM
  build, and the full test suite are all unchanged and green.
  - `nalgebra` 0.34 → 0.35
  - `rand` 0.9 → 0.10
  - `rand_distr` 0.5 → 0.6
  - `getrandom` 0.3 → 0.4 (the wasm32 `wasm_js` backend)

- **Monte Carlo RNG stream changed (seeded-output note).** `rand` 0.10 revised
  its internal generator, so a given seed now produces a different sequence of
  draws than 0.22.6 and earlier. Monte Carlo *statistical* results — means,
  spreads, confidence intervals — are unchanged, but if you depend on a fixed
  seed reproducing an exact set of simulated shots, those specific values will
  differ from prior versions. This is a one-time shift from the dependency
  update, not a change in the physics.

  (`printpdf` 0.9 was evaluated and deliberately **not** taken: it is a full
  API rewrite that would require rewriting the dope-card PDF layer for no
  user-facing gain, so `printpdf` stays pinned at 0.7.)

## [0.22.6] - 2026-07-05

### Fixed

- **Zero-day atmosphere appeared to have no effect at a short auto-zero.** With
  `--auto-zero 100`, a base command and one adding extreme zero-day atmosphere
  (`--zero-temperature 20 --zero-pressure 20 --zero-humidity 90 --zero-altitude 12000`)
  produced byte-identical output. The atmosphere *was* reaching the zero solve, but the
  zero-angle search converged as soon as the height error at the zero distance was under
  1 mm (~0.037 MOA at 100 yd) — coarser than the small zero-day-density effect there — so
  two very different atmospheres rounded to the same zero angle. Tightened the convergence
  to 0.1 mm (angle tolerance 1e-7 rad), so the effect now resolves even at short zeros. It
  remains physically small (a 100 yd zero is nearly density-independent: ~0.04" at 100 yd,
  ~0.2" at 490 yd for those extreme conditions) but is no longer quantized to zero. This
  also makes the zero angle itself more precise. Reported by an external user.

## [0.22.5] - 2026-07-05

### Added

- **Decoupled powder temperature for the curve, symmetric on shot and zero days.** The
  `--powder-temp-curve` maps *powder* temperature to velocity, but was interpolated at the
  ambient *air* temperature. Powder isn't always at air temperature (warmed in a chamber,
  cooled in a pocket), so velocity and air density now use separate temperatures:
  - `--powder-temp` (now accepts no value / is optional) sets the powder temperature the
    curve is interpolated at on the shot side; it defaults to `--temperature` when omitted.
    (With the linear `--powder-temp-sensitivity` model instead, `--powder-temp` keeps its
    existing "reference temperature" meaning, default 70°F/21°C — the models are mutually
    exclusive.)
  - `--zero-powder-temp` (new) sets the zero-day powder temperature for the curve on the
    `--auto-zero` side, defaulting to `--zero-temperature`. An explicit `--zero-velocity`
    still takes precedence.
  - Air temperature (`--temperature` / `--zero-temperature`) continues to exclusively drive
    air density. Fully backward compatible: with neither flag, the curve is looked up at the
    air temperature exactly as before. Native CLI + WASM; integration tests added.

### Fixed

- **WASM: an explicit `--zero-velocity` was overridden by a shot-day `--powder-temp-curve`
  during the zero solve.** The WASM zero solve cloned the shot inputs (carrying the curve),
  which re-interpolated over the supplied zero velocity. `--zero-velocity` now disables the
  curve for the zero solve, matching the native CLI.

## [0.22.4] - 2026-07-05

### Fixed

- **The WASM command surface now rejects unknown flags instead of silently ignoring
  them.** The hand-rolled WASM argument parser used a catch-all that dropped any
  unrecognized token, so a typo or a flag that isn't wired into the WASM surface looked
  like a successful no-op — making "no error" an unreliable signal that a flag is active.
  Unrecognized `--flags` now return an `Unknown flag: <name>` error, matching the native
  CLI's clap behavior. Applied to all WASM subcommands (trajectory, zero, monte-carlo,
  estimate-bc).

## [0.22.3] - 2026-07-04

### Added

- **MOA support for the PDF dope card (`--adjustment-unit mil|moa`).** The printable
  dope card was MIL-only; its Drop/Wind/Lead columns now render in MIL (default) or MOA,
  with the column sub-headers labeled accordingly. The MIL and MOA paths share one
  converter (`drop_to_adjustment`), and the moving-target lead is computed in the same
  unit. Backward compatible — the default is MIL. (MBA-724)

### Changed

- Unified the dope-card angular conversion on `drop_to_adjustment`, removing the
  now-redundant MIL-only `yards_to_mil` / `calculate_lead_mil` helpers.

## [0.22.2] - 2026-07-04

### Added

- **`--powder-temp-curve`: a measured powder-temperature → muzzle-velocity table.** The
  linear `--powder-temp-sensitivity` model assumes a constant fps/°F, but real powders are
  non-linear (temperature-stable powders flatten; others steepen when hot). This flag takes
  comma-separated `TEMP:VELOCITY` points (e.g. `"40:2620,70:2700,100:2760"`, in °F/fps or
  °C/m·s⁻¹ per `--units`) and interpolates the muzzle velocity at the ambient `--temperature`,
  clamped at the endpoints (no extrapolation beyond measured data). When supplied it
  supersedes the linear sensitivity model. It is data-driven — the shooter enters points they
  actually chronographed — rather than a guessed curve shape, and mirrors the existing
  `bc_segments` interpolation design.
- The curve composes with `--auto-zero`: with `--zero-temperature`, the zero angle is solved
  using the curve's velocity at the zeroing-day temperature (an explicit `--zero-velocity`
  still takes precedence). Wired into both the native CLI and the WASM command surface, with
  integration tests for exact points, interpolation, endpoint clamping, and backward
  compatibility.

## [0.22.1] - 2026-07-04

### Added

- **Zero-day condition overrides for `--auto-zero`.** New optional flags let the zero
  **angle** be solved under the conditions the rifle was actually zeroed in, while the
  trajectory runs under the current shot-day conditions — correctly modeling the point-of-
  impact shift when you sight in on one day/velocity and shoot on another (e.g. a cold vs.
  warm powder temperature drawn from a powder-temp/velocity table):
  - `--zero-velocity` — zero-day muzzle velocity (fps / m·s⁻¹)
  - `--zero-temperature` — zero-day air temperature (°F / °C)
  - `--zero-pressure` — zero-day barometric pressure (inHg / hPa)
  - `--zero-humidity` — zero-day relative humidity (percent)
  - `--zero-altitude` — zero-day altitude (feet / meters)

  Each flag is independently optional; any omitted flag falls back to the corresponding
  shot-day value, so supplying none of them reproduces the previous single-condition
  behavior exactly (verified: zero output diff). Wired into both the native CLI and the
  WASM command surface.

## [0.22.0] - 2026-07-01

A large correctness pass from an adversarial logic/math audit of the whole crate (44
confirmed findings across drag, atmosphere, ground/zero geometry, wind, spin/rotational
physics, Monte Carlo, and cross-surface consistency). This release changes numeric output
for most trajectories — re-validate any downstream reference tables. Highest-impact items
marked ⚠️.

### Fixed

**Drag & atmosphere**
- **⚠️ Ground plane no longer double-counts bore height.** `ground_threshold` was set to
  `-bore_height` while the solver already starts the bullet at a ground-referenced
  `muzzle_height`, so flat-fire shots flew until `2×bore_height` below the muzzle instead
  of 1 — ground-impact range and time-of-flight were overstated (~37%/~45% in a typical
  flat-fire case). Ground is now `y = 0` in all CLI/FFI/WASM/Monte-Carlo call sites.
- **⚠️ Removed a transonic drag-rise double-count.** The G1/G7 table Cd already embeds the
  full McCoy transonic rise; an additional multiplier (up to 2.5×, applied unconditionally
  near Mach 1) was stacking a second rise on top of it, inflating drop and reducing retained
  velocity for any shot touching Mach 0.85–1.2 — and for the entire flight of subsonic loads
  (.22LR, subsonic .300BLK, etc).
- **⚠️ Corrected `CD_TO_RETARD` to the ICAO-referenced value** (`2.08551e-4`, was the Army
  Standard Metro constant `2.049e-4`), matching density normalized to ICAO sea level
  (1.225 kg/m³). Drag was uniformly ~1.75% low for any ICAO-referenced BC.
- **Mach number and air density now use the same resolved atmosphere.** Speed-of-sound was
  computed from the raw input temperature while density used the ICAO-lapse-resolved
  station temperature at `--altitude`, so drag was evaluated at the wrong Mach for
  altitude shots with default atmosphere (a few percent error near transonic).
- **Miller Sg density correction, and the fast/Monte-Carlo Mach lookup, now use the same
  resolved station atmosphere** instead of the raw (often default sea-level) input —
  affects stability classification, spin drift, and Magnus at altitude.
- Moist-air speed-of-sound and CIPM-2007 enhancement/compressibility coefficients corrected
  (were using a specific-humidity coefficient with a mole fraction, and hPa where the CIPM
  constants expect Pa); both errors were small (<1.5%) but real.
- Magnus moment coefficient is now continuous at Mach 1.2 (was a 33% discontinuous jump
  between the transonic and supersonic branches).

**Zeroing & geometry**
- **`zero` subcommand now zeroes to the line of sight**, not the bore line — was ignoring
  `--sight-height` entirely (~1.9 MOA error at typical sight heights).
- **`--auto-zero` now solves with the user's actual drag model and atmosphere**, not a
  hardcoded G1/ISA-sea-level zero — a G7 auto-zero previously struck several inches off
  the intended zero distance.
- **`sight_adjustment_moa` sign fixed** (was subtracting the sight-height correction instead
  of adding it — told users to dial the wrong direction).
- **`point_blank_range` fixed** — was reading the lateral (always-zero) axis with a
  bore-referenced threshold; now downrange, referenced to the line of sight.
- **`true-velocity` drop formula no longer double-counts barrel elevation** and no longer
  flips the sight-height sign (was up to +11% effective-velocity error).
- **`--shooting-angle` (inclined fire) is now applied** — previously accepted by every
  surface (CLI/FFI/WASM) and by the low-level fast/library-binding integration path, but
  read by no solver; gravity is now rotated into the shot-aligned frame everywhere,
  including `fast_integrate_with_segments`.
- Trajectory `bullet_length` now honors `--bullet-length` instead of a hardcoded 4.5-caliber
  estimate, so stability/spin-drift/aerodynamic-jump match the `stability` subcommand for
  the same bullet.

**Monte Carlo**
- **CEP is now the median target-plane radial miss** (was `range_std_dev × 1.1774`, off by
  up to ~100×).
- **Fell-short samples are no longer misclassified as hits** — a downrange shortfall was
  being compared against the target-plane hit radius.
- **Vertical angular dispersion is no longer sampled twice** — muzzle-angle jitter and an
  independent "pointing error" of the same magnitude were both applied, inflating vertical
  spread ~41%.
- **`max_range` is now sized to the target**, so simulations no longer silently truncate at
  the 1000 m integrator default on longer shots.
- **FFI and WASM Monte Carlo now use the same ground/bore convention as the CLI** (previously
  only the CLI had a real ground-impact range; FFI/WASM reported the integrator cap with
  zero standard deviation).
- **FFI Monte Carlo now honors its `atmosphere` argument** instead of discarding it and
  running every sample at ICAO sea level.

**Wind**
- Wind beyond the last `--wind-segment` now correctly goes to zero on the shear-enabled fast
  path (was falling back to the first segment's wind).
- `fast_integrate_with_segments` (the library/binding entry point) now actually steps
  through wind segments by downrange distance instead of applying only the first segment
  for the whole flight.

**Cross-surface consistency (CLI vs FFI vs WASM)**
- `estimate-bc` drop values are now inches on both CLI and WASM (CLI was parsing them as
  yards — a 36× unit mismatch).
- `--twist-rate` is mm/turn in metric mode on WASM, matching the CLI (MBA-970).
- Powder-temperature modeling now matches between the native solver and WASM (WASM
  previously used a hardcoded 70°F/21°C reference and ignored `--temperature` entirely).
- `--powder-temp`'s default no longer gets reinterpreted as 70°C in metric mode.
- WASM now sets `ground_threshold` on `trajectory` (was the -100 m library default, a ~3.5×
  disagreement with the CLI on the same command).
- WASM's Drop column (table/JSON/CSV) is now relative to the line of sight instead of the
  bore line.
- WASM's default wind direction is now 0° (headwind), matching the CLI, instead of 90°
  (full crosswind).
- FFI trajectory samples now report real `mach` and `spin_rate_rps` instead of hardcoded 0.0.
- FFI `ballistics_quick_trajectory` drop is now referenced to the sight line, not the bore.
- Metric-mode `--bc-table`/`--bc-table-dir` lookups now convert to the tables' native
  inches/fps units instead of feeding them mm/m-s.
- `--bc-adjustment` now actually affects the trajectory when a `--bc-table` is also
  supplied (previously affected only the displayed BC).
- Metric-mode PDF dope cards now convert to the card's imperial fields instead of writing
  raw metric values under imperial labels.
- Density-altitude on the PDF dope card no longer applies the 120 ft/°C rule to a Fahrenheit
  delta (was overstating the temperature correction ~1.8×).
- `monte-carlo` help text now documents per-`--units` values instead of claiming SI
  regardless of the active unit system.

**Numerical**
- `fast_integrate`'s `FastSolution::sol()` no longer returns NaN when queried at its own
  reported event time (was pushing a duplicate final (time, state) sample).
- The sampled-trajectory `Apex` flag is no longer placed on the first sample for flat or
  descending shots that never rise above the muzzle.

### Performance
- Speed-of-sound/temperature/pressure are now resolved once per trajectory solve and
  threaded through drag and Magnus calculations, instead of being re-resolved (including a
  vapor-pressure polynomial) on every RK4/RK45 stage.

## [0.21.5] - 2026-06-27

Two monte-carlo correctness fixes (MBA-967, MBA-971).

### Fixed
- **monte-carlo "Mean Range" is now meaningful and target-independent (MBA-967).**
  Simulations used `BallisticInputs::default()` (muzzle_height 0, ground_threshold -100 m)
  so each sim flew to the integrator's range cap instead of a real ground impact — "Mean
  Range" reported the cap (~1000 m), not the trajectory's ground-impact range. And a
  per-sample `max_range < target_distance` skip tied the range statistic to
  `--target-distance`. MC now uses the same bore-height/ground convention as `trajectory`
  (1.5 m) and computes range/velocity over all samples, so Mean Range matches `trajectory`
  for the same physics and no longer changes with the target distance.
- **monte-carlo `hit_probability` unified across the CLI and FFI (MBA-971).** The CLI
  counted ground-impact ranges within 1 m of the target (a range-precision notion that read
  0% for any target short of the impact range) while the FFI used a position notion with a
  redundant clause. Both now use a single `MonteCarloResults::hit_probability(radius)`
  method — the fraction of samples landing within the hit radius of the point of aim at the
  target plane. Samples that fall short are recorded as definite misses, so an unreachable
  target correctly yields ~0% (was a spurious ~100%).

### Added
- **`monte-carlo --target-radius` flag** (default 0.3 m, unit-aware) to define the hit zone
  used by hit probability, instead of a hardcoded radius.

## [0.21.4] - 2026-06-26

Ten CLI/correctness fixes surfaced by an adversarial edge-case sweep (MBA-960..966,
968, 969, 970). Two are behavioral changes existing consumers should note:

### Fixed
- **⚠️ Metric mode no longer runs in a near-vacuum (MBA-960/961).** The CLI atmosphere
  defaults (`29.92` / `59.0`) were imperial literals applied before `--units` was parsed,
  so in metric mode they were read as 29.92 hPa (~24 km) and 59 °C — drag ~30× too low,
  impact velocity ~42% high. `--pressure`/`--temperature` are now resolved and validated
  *after* `--units` (per-unit defaults + ranges). **Metric trajectories without explicit
  atmosphere flags now produce different (correct) output than before.** Also fixes the
  `--pressure` validator (was an hPa range applied to imperial inHg: rejected valid `14`,
  accepted an hPa typo like `1013`).
- **⚠️ JSON output now honors `--units` (MBA-962).** The JSON formatter always emitted raw
  SI regardless of `--units` (table/csv were already correct). It now converts to the
  requested units and adds an explicit `"units"` field. **JSON consumers using `--units
  imperial` previously received metric numbers; they now receive imperial.**
- **`--use-powder-sensitivity` now affects the trajectory (MBA-963).** It was a no-op on
  the CLI solve path (the muzzle-velocity adjustment was computed but never applied); the
  per-degree sensitivity delta was also mis-converted. Now wired into `TrajectorySolver`.
- **`spin_drift` summary is consistent with the applied path (MBA-964).** It reported
  `null` while the path silently applied drift from a default 1:12 twist.
- **`--wind-shear-model` is now honored (MBA-965).** The selector was discarded (everything
  ran as PowerLaw) and invalid values were accepted; models are now distinct and validated.
- **Pitch-damping / precession diagnostics are serialized (MBA-966).** `--enable-pitch-damping`
  / `--enable-precession` computed diagnostics that the CLI never emitted; now in JSON.
- **Default RK45 reaches `--max-range` exactly (MBA-968).** It stopped ~2% short (no
  boundary interpolation), disagreeing with Euler/RK4-fixed; the final state is now
  interpolated to the boundary.
- **Time-cap runs are labeled "no impact" (MBA-969).** An upward shot with
  `--ignore-ground-impact` reported a bogus impact at the hidden 100 s integration cap.
- **`--twist-rate` is mm/turn in metric for `trajectory` (MBA-970).** It was read as inches
  regardless of `--units`, so a metric 1:10 twist gave zero stability; now matches the
  `stability` subcommand.

## [0.21.3] - 2026-06-25

### Fixed
- **Direct-atmosphere mode no longer rejected as degenerate (0.21.2 regression).** The
  0.21.2 degenerate-atmosphere guard rejected the legitimate direct-atmosphere tuple
  `(air_density, speed_of_sound, 0.0, 0.0)` because slots 2 and 3 are `0.0` sentinels and
  the guard treated "pressure ≤ 0" as non-physical. The guard now recognizes direct mode
  (real density < 2.0 kg/m³ and speed of sound > 200 m/s with zero pressure/ratio slots)
  and only rejects genuinely non-physical input. Test: `fast_path_rejects_degenerate_atmosphere`
  now also asserts direct mode succeeds.
- **Transonic Mach transitions are now flagged in sampled output.** The trajectory sampler's
  `transonic_distances` was a `Vec::new()` placeholder (a TODO), so no sampled point ever
  received a `MachTransition` flag regardless of trajectory. The Euler / RK4 / RK45 solvers
  now record the downrange distances where the projectile crosses Mach 1.2 (transonic) and
  Mach 1.0 (subsonic), and `add_trajectory_flags` marks the nearest sample at each crossing.
  Test: `transonic_crossing_flags_a_sampled_point`.
- **MBA-717: fast/MC path no longer hardcodes bullet geometry.** `build_inputs` used fixed
  placeholders (0.308" diameter / 1.24" length / 10" twist) because `TrajectoryParams`
  didn't carry the geometry, so spin-drift / Magnus / stability on that path ignored the
  real bullet. `TrajectoryParams` now carries `bullet_diameter` / `bullet_length` /
  `twist_rate`, plumbed from the inputs. (Behavioral impact is small today — the fast-path
  yaw-of-repose has no angular state, so its Magnus/spin-drift is ~0 — but the data is now
  correct for callers that do use it and for future work on that path.)

### Changed
- **MBA-722: humidity-scale convention documented + centralized.** `BallisticInputs.humidity`
  is a 0–1 fraction while `AtmosphericConditions.humidity` is a 0–100 percent (the scale the
  atmosphere density helpers expect) — an easy 100× footgun. Added a `humidity_percent()`
  helper that does the clamped 0–1 → 0–100 conversion, used it at the Monte-Carlo boundary,
  and cross-documented both fields. No numerical change (existing paths already converted).
- **Documented the dual-mode `atmo_params` tuple.** The atmosphere 4-tuple has two modes
  (standard `(alt, temp_c, pressure_hPa, density_ratio)` vs direct `(density, sound, 0, 0)`)
  and slot 3 is a density RATIO that rides in the `humidity` field — an easy footgun. Added
  doc comments at `TrajectoryParams.atmos_params`, `FastIntegrationParams.atmo_params`, and
  the `atmo_is_physical` guard spelling out both modes. Documentation only.

## [0.21.2] - 2026-06-25

### Changed
- **Coriolis is now independent of the advanced-effects umbrella on the fast/MC path.**
  `fast_integrate_with_segments` previously gated its Earth-rotation ω — and spin-drift /
  Magnus — together on `enable_advanced_effects`, so a caller could not request
  Coriolis-only. The ω is now gated on `enable_coriolis` (+ a latitude), and Magnus on
  `enable_magnus`, matching the RK4 solver. A caller can enable Coriolis with
  `enable_advanced_effects = false`. Tests: `fast_path_coriolis_independent_of_advanced_effects`.

### Fixed
- **Degenerate atmosphere no longer reports success.** A non-physical `atmo_params`
  (pressure ≤ 0 or non-finite — typically a unit mix-up, e.g. inHg passed where hPa is
  expected) yielded zero/NaN air density and silently truncated the fast path to a
  single-point stub trajectory marked `success = true`. The fast entrypoints now validate
  the atmosphere up front and return `success = false` for degenerate input. Test:
  `fast_path_rejects_degenerate_atmosphere`.

## [0.21.1] - 2026-06-24

### Fixed
- **Coriolis shot direction on the fast / Monte-Carlo path.** 0.21.0 fixed the RK4
  solver but missed a third Earth-rotation construction site: `fast_integrate_with_segments`
  (the path the Python binding uses) still built ω from `azimuth_angle` instead of
  `shot_azimuth`, so east and west shots were identical there. Now uses `shot_azimuth`,
  matching the RK4 and `fast_integrate` paths — directional Coriolis (Eötvös) is applied
  consistently across all solver paths. Regression test
  `fast_trajectory::tests::fast_path_coriolis_uses_shot_direction`. (This corrects the
  "fast/MC path is still a no-op" caveat noted in 0.21.0.)

### Changed
- **DOWNSTREAM:** the fast/MC path now changes output for Coriolis shots with a non-North
  bearing (north / unset unchanged). The Python binding accepts a `shot_direction` key
  (degrees, 0=N) to drive it; pass it alongside `latitude` + advanced effects.

## [0.21.0] - 2026-06-24

### Fixed
- **Coriolis now responds to shot direction (Eötvös effect).** `--shot-direction` was
  stored but never reached the trajectory solver, so the Coriolis correction was always
  computed as a due-North shot — east- and west-fired shots produced identical results.
  The solver now carries the firing compass bearing in a dedicated `shot_azimuth` input
  (distinct from `azimuth_angle`, the small aiming offset that rotates the launch
  velocity), and both the RK4 solver and the fast/Monte-Carlo path use it for the
  Earth-rotation vector. An east shot now gets the upward Eötvös term (`+2Ω·cosφ·v_east`,
  shoots higher) and a west shot the downward one (shoots lower); lateral drift is
  unchanged. Regression test `coriolis_direction_tests::eotvos_east_higher_than_west`.

### Changed
- **DOWNSTREAM:** numerical output changes for Coriolis shots with a non-North
  `--shot-direction` (previously those were silently treated as North; North / unset is
  unchanged). FFI `FFIBallisticInputs` gains an appended `shot_azimuth` field (radians,
  0=N) — existing field offsets are preserved, but FFI consumers that construct the full
  struct must add it. WASM `runCommand` now accepts `--shot-direction`. The fast/MC path
  still does not plumb latitude/bearing on its own (`build_inputs` hardwires
  `latitude: None`), so directional Coriolis there remains out of scope for this release.

## [0.20.0] - 2026-06-21

Dependency modernization — a coordinated upgrade of the major dependencies. No new
features, CLI flags, or physics; numerical trajectory output is unchanged. The one
observable difference is in Monte Carlo: under the new RNG the per-run sample
*sequence* differs, but the statistical results (mean, std-dev, CEP, confidence
ellipse) are unchanged.

### Changed
- **`ndarray` 0.15 → 0.17** and **`ndarray-npy` 0.8 → 0.10** (upgraded together; the
  drag-table `.npy` loader is unaffected).
- **`rand` 0.8 → 0.9** and **`rand_distr` 0.4 → 0.5**. Monte Carlo uses an unseeded
  RNG, so this only changes the sample sequence, not the statistical behavior.
- **`ureq` 2.12 → 3.1** (online feature). Migrated to the 3.x API; the public error
  mapping (`ServerError(code, body)`, timeout vs. network) and the unbounded
  BC-table read are preserved.
- **`getrandom` 0.2 → 0.3** with the `wasm_js` backend feature (WASM builds).
- CI: `actions/upload-artifact` 6 → 7, `actions/download-artifact` 7 → 8,
  `cross-platform-actions/action` 0.30 → 0.32.

## [0.19.0] - 2026-06-20

Adds downrange-segmented wind to the CLI and WASM, and **corrects the wind-direction
sign on the `cli_api`/`TrajectorySolver` path** (a breaking output change — see below).

### Added
- **Downrange-segmented wind** — wind can now vary along the trajectory (e.g. a muzzle
  reading plus downrange sensor stations).
  - CLI: repeatable `--wind-segment SPEED:ANGLE:UNTIL_DISTANCE` on the `trajectory`
    command. SPEED and UNTIL_DISTANCE follow `--units` (mph & yards imperial, m/s &
    meters metric); ANGLE is degrees in the wind-FROM convention (same as
    `--wind-direction`). Each segment applies from the previous boundary out to its
    UNTIL_DISTANCE; wind is a step function with **zero wind beyond the last segment**
    (a coverage warning is printed when the segments don't reach `--max-range`).
    Segments **override** scalar `--wind-speed`/`--wind-direction`, and are **not
    compatible with `--enable-wind-shear`** (rejected with an error).
  - WASM: `runCommand` accepts the same repeatable `--wind-segment` flag, and the
    `Calculator` gains `addWindSegment(speedMph, directionDeg, untilYards)` and
    `clearWindSegments()`.
  - API: `TrajectorySolver::set_wind_segments(Vec<WindSegment>)` performs a per-step
    downrange lookup via `WindSock`, preserving all solver physics (Coriolis, spin
    drift, aerodynamic jump, zero-finding, full `TrajectoryResult` output). A solve
    with no segments is numerically identical to before. New public
    `wind::parse_wind_segment_str(&str, imperial)`.

### Fixed
- **CLI `--twist-right` can now select left-hand twist.** It was a presence-only flag
  (always true; `--twist-right false` errored), so left-hand twist — and the
  aerodynamic-jump direction reversal it produces — was unreachable from the CLI. It now
  takes an optional value: `--twist-right` / `--twist-right true` = right-hand (default),
  `--twist-right false` = left-hand (the bare flag still works, so existing usage is
  unaffected).

### Changed (BREAKING)
- **Wind direction sign corrected on the `cli_api`/`TrajectorySolver` path.** The scalar
  wind vector was built with the opposite sign to the standard wind-clock convention
  (and to the engine's own `WindSock`, the Monte-Carlo path, and the Python/Ruby
  bindings): `--wind-direction 0` produced a *tailwind* and `90°` drifted the bullet the
  wrong way. It is now correct: **0° = headwind, 90° = from the right (drifts left),
  180° = tailwind, 270° = from the left** — matching `WindSock`/the bindings.
  **Trajectories computed with a non-zero wind direction will change** (head/tail effect
  and crosswind drift direction flip); no change when wind speed is 0. This affects
  **every consumer of the `TrajectorySolver`/cli_api path**: the CLI `trajectory`,
  `wind-card`, `range-table`, and `come-ups` subcommands; the WASM `runCommand` and
  `Calculator`; the C FFI (`FFIWindConditions.direction`, for iOS/Android callers); and
  any direct `TrajectorySolver` user. The Monte-Carlo path and the Python/Ruby bindings
  already used the correct convention and are unchanged. The aerodynamic-jump crosswind sign
  on this path was aligned to match (right twist + wind-from-right → impact up, drift
  left), bringing `cli_api` into agreement with the fast-integrate path. The wind-shear
  path was already sign-aligned with the scalar path and flips with it.
- The `--wind-direction` help text was corrected (previously the inaccurate
  "0=North, 90=East").

## [0.18.3] - 2026-06-19

Extends aerodynamic jump (MBA-959) to the fast-integrate path. Default-off; no change to existing trajectories.

### Added
- **Aerodynamic jump in `fast_trajectory::fast_integrate`** — the fast fixed-step kernel (used by the `ballistics-engine-py` `fast_integrate` binding and the Monte-Carlo path) now applies aerodynamic jump when `enable_aerodynamic_jump` is set, by perturbing the prebuilt launch velocity vertically. New public `fast_trajectory::aerodynamic_jump_launch_offset_rad(inputs, atmo_params)` returns the Litz vertical offset (radians) from the engine's Miller Sg; crosswind is taken from `wind_speed`/`wind_angle` (BallisticInputs convention: 0 = headwind, +90° = from the right). Previously only the `cli_api::TrajectorySolver` (Euler/RK4/RK45) path applied AJ; the fast path ignored the flag.

## [0.18.2] - 2026-06-19

Adds an opt-in aerodynamic-jump effect (MBA-959). Default-off and additive — no change to existing trajectories.

### Added
- **Aerodynamic jump** (`enable_aerodynamic_jump`, default off): the vertical point-of-impact shift a crosswind imparts to a spin-stabilized bullet, applied as a muzzle launch-angle perturbation in the solver. Uses Bryan Litz's estimator `Y = 0.01·Sg − 0.0024·L + 0.032` (MOA per mph of crosswind), fed by the engine's own Miller stability factor (Sg) and bullet length in calibers. Exposed on `TrajectoryResult.aerodynamic_jump`, via the CLI `--enable-aerodynamic-jump` flag, and as the public `aerodynamic_jump::litz_crosswind_jump_moa()`. The jump is purely vertical; direction follows Litz (right twist: crosswind from the right → impact up). The zero is found on the bare bore, so the jump appears as an additive POI shift rather than being absorbed by the zero. Most accurate near Sg ≈ 1.75 (Litz regression validity).

### Notes
- The legacy heuristic `aerodynamic_jump::calculate_aerodynamic_jump` is retained for backward compatibility but is superseded by the Litz estimator and is no longer used by the solver.

## [0.18.1] - 2026-06-18

Two follow-up fixes after 0.18.0. Neither changes trajectory output.

### Fixed
- **BCCR drag-correction loader** now verifies the stored CRC32 (a standard IEEE/zlib CRC over the data section) instead of reading and discarding it — corrupt-but-in-range files are rejected with `ChecksumMismatch` instead of loading silently. Gated on a non-zero stored checksum so older checksum-less files still load (MBA-953).
- **Precession/nutation angular diagnostics** — corrected four dimensional errors: the precession and nutation frequencies are now rad/s via the standard epicyclic form `φ = (Iₓp/2Iy)[1 ± √(1 − 1/Sg)]` (with the dimensionless Miller stability factor), and the yaw no longer random-walks. Diagnostic-only — these feed only the opt-in `max_yaw_angle` / `max_precession_angle` outputs (`--enable-precession-nutation`); no trajectory change (MBA-941).

## [0.18.0] - 2026-06-18

A cross-solver consistency and physics-correction pass (JIRA MBA-938..958). 17 commits since v0.17.0; build and full test suite (210+) green. Several changes correct real bugs that **shift numerical results** on the Coriolis, transonic/subsonic drag, spin-drift-at-altitude, and WASM-zero paths — see Changed and validate against a reference before deploying downstream.

### Fixed
- **Coriolis deflection direction** — corrected to the physical `-2 Ω×v` with the right lateral ω sign; a Northern-hemisphere shot now drifts right (was left), validated vs py_ballisticcalc. The fast/Monte-Carlo path (used by the Python/Ruby bindings) applied no Coriolis at all and now matches the default solver to ~1%.
- **Transonic drag resolution** — the engine silently used a coarse 21-point G1/G7 fallback (a runtime loader looked for a directory that never exists at runtime); the high-resolution 79/84-point tables are now baked in via `include_str!`. Transonic drop error vs py_ballisticcalc fell from +43" to +3" at 1300 yd.
- **Custom drag tables** (`custom_drag_table`) are now honored by all three solvers (were plumbed onto the inputs but never read — the G-model was always used).
- **Named bullet shapes** (boat/round/flat in `bullet_model`) now drive the transonic shape on every solver (the integrate_trajectory path ignored the name and used a caliber/weight heuristic).
- **`use_form_factor`** is honored consistently across all three solvers (was derivatives-only; no-op by default).
- **WASM standalone zero** targets the line of sight (sight height), not the bore line — it was off by the sight-height angle (~2 MOA at 100 yd / 2" sight).
- **PDF output** is gated behind the `pdf` feature so `--no-default-features` builds the binary.
- **Sampled `--full` CSV** reports drop/drift in inches (Imperial) / meters (Metric), matching the table and API (was yards).
- Removed a dead WASM `--wind-dir-std` setter; documented the wind-direction-spread approximation.

### Changed
> **FLAGGED for downstream:** these correct real bugs but **shift numerical output**. Re-validate dope and any cached/expected outputs.
- **Coriolis** — direction fix plus the previously-absent Monte-Carlo/binding application; affects any Coriolis-enabled trajectory.
- **Transonic/subsonic drag** — the high-resolution G1/G7 tables shift drag below ~Mach 1.3; the low-velocity **Reynolds correction was removed** (it ran on only one of three solvers, py_ballisticcalc does not model it, and the default solver already matches pbc subsonically), shifting subsonic (<200 m/s) shots on the binding path.
- **Spin drift at altitude** — the Miller stability density correction is now the canonical linear `(T/T0)(P0/P)` (was `sqrt`), raising spin drift at altitude (sea level unchanged). The same correction now also feeds the Magnus / yaw-of-repose Sg (off by default).
- **WASM zero** shifts by the sight-height angle (~2 MOA at 100 yd) — now a proper sight-line zero.

### Added
- `transonic_drag::resolve_projectile_shape(...)` — shared bullet-shape resolution (name first, then heuristic) used by all three solvers.
- `TrajectoryParams.ground_threshold` — the integrate_trajectory ground plane is configurable (was hardcoded `-1000.0`; default preserved).

### Performance (output-identical)
- Velocity-BC segments are estimated **once** at solver setup on the integrate_trajectory path (the Python-binding hot path) instead of being rebuilt — allocating a string + a vector — on every derivative evaluation. Byte-identical output.

## [0.17.0] - 2026-06-17

An autonomous adversarial hardening pass plus a follow-up altitude/temperature fix. 82 fixes across 78 commits since v0.16.0; build and full test suite green. Most changes are correctness/robustness fixes that preserve output, but several correct real bugs that **shift numerical results** — see Changed below and validate against a reference before deploying downstream.

### Fixed
- **RK45 time-of-flight desync** — the default solver advanced `time` by the *next* step's dt, corrupting time_of_flight and per-point/sample times.
- **Arden-Buck vapor pressure** — the linear factor sat outside `exp()`, over-estimating saturation pressure (~7x) and corrupting humidity-dependent air density.
- **Gyroscopic spin-drift twist sign** — applied twice (canceling), so left-twist barrels drifted the wrong way (right-twist/default unchanged).
- **API twist-rate unit** — sent in meters where inches/turn was expected (~33x off); now sent verbatim as inches/turn across API/FFI/WASM.
- **Wind-layer extrapolation** — above the top custom layer the profile extrapolated garbage instead of clamping; WindSock cursor no longer skips segments on large query jumps.
- **Transonic drag** applied exactly once everywhere (was double-applied in `derivatives.rs`, missing in `fast_trajectory.rs`, shape-misclassified in `cli_api.rs`); **Reynolds drag** applied once and capped at 5x (was double-applied / uncapped ~16x spike).
- **Euler solver** now uses the shared acceleration kernel — previously ignored the ballistic coefficient (diverging up to ~2.3x) and omitted Magnus/Coriolis.
- **Projectile shape (default solver)** received caliber in meters instead of inches, under-applying transonic drag for G1/G2/G5/G6/G8/GI/GS.
- **Monte Carlo** — fixed long-range truncation and a 100x humidity mis-scale (fraction vs percent); BC-correction table no longer silently zeros the BC when queried with the other drag model.
- **Drag model selection** — G2/G5/GI/GS now explicitly alias to G1 (documented).
- **Output** — corrected dope-card Drop MIL sign, `--full` table downrange/drift column swap, dope-card footer weekday (was off by 4 days), stability min-twist units label, and estimate-bc verification range.
- **WASM** — auto-zero solves to line-of-sight (not bore); wind direction consumed as radians; Monte Carlo honors `--drag-model`; full-trajectory JSON keys fixed; wasm32 target now compiles; help text synced to the parser and dead zero flags dropped.
- **Edge cases / divide-by-zero / NaN guards** — sectional-density and BC zero divisors, aero-jump zero/negative twist and `mass_kg<=0`, moist-air speed of sound at zero pressure, air density at non-positive pressure, zero-angle solver reporting non-convergence as success.
- **Robustness** — BC5D/BCCR table loaders use checked-arithmetic dimension bounds and NaN-safe lookups; FFI stops leaking a `CString` per version call and caps oversized/negative Monte Carlo sim counts; fixed panics on PDF UTF-8 header truncation, `percentile(p>1)` OOB, and Euler zero-relative-velocity.
- **Input validation** — CLI rejects non-finite numeric arguments; `target_distance` must be finite and positive (was `> 0`, letting `+inf` through); only `-inf` maps to the ignore-ground sentinel.

### Changed
> **FLAGGED for downstream:** these correct real bugs but **shift numerical output** — altitude, air-density, gravity, and transonic results all move. Re-validate dope and any cached/expected outputs.
- **Air density unified** across all three integrators — `cli_api` no longer double-counts altitude (`exp(-alt/8000)` atop station pressure) and now applies humidity; Monte Carlo no longer reprojects shooter-altitude density to sea level. Shifts *every* trajectory (~0.32% at sea level, larger at altitude).
- **Altitude now drives both station pressure and temperature** — with a default (15 °C / sea-level) input, `--altitude` derives ICAO station pressure *and* applies the −6.5 °C/km lapse rate; an explicit pressure or temperature stays authoritative. Previously altitude-only ran too warm/thin. Validated against py_ballisticcalc to ~0.04% (0–3000 m).
- **Gravity** is the SI-canonical `9.80665` in the default RK4/RK45 kernel (was `9.81`).
- **Transonic single-application** and **Reynolds once + capped** change near-Mach-1 and Monte Carlo drag.
- **Spin twist sign** and **aero-jump Sg** (kg→grains, was ~1000x off) affect opt-in spin/jump diagnostics; left-twist spin-drift direction changes.
- **Default `bullet_length` 4.0→4.5x caliber** (now unified across library/FFI/WASM) — shifts opt-in spin/Magnus/Miller diagnostics only, not the base trajectory.
- **Zero solver returns `Err` on non-convergence** (was a best-effort `Ok(angle)`) — downstream callers must handle `Err`.
- **WASM default drag model G7→G1** to match the G1-scale default BC.
- **Monte Carlo range/velocity means exclude short-falling draws** when no explicit target is given (keeps CEP measurable and FFI arrays equal-length); pass an explicit target for unbiased per-target stats.

### Added
- `atmosphere::resolve_station_pressure(pressure_hpa, altitude_m) -> Option<f64>` — an explicit station pressure stays authoritative; a default pressure with real altitude derives the ICAO station pressure.
- `atmosphere::resolve_station_temperature(temperature_c, altitude_m) -> Option<f64>` — mirrors the above for temperature (returns `None` at the 15 °C default so the ICAO lapse rate applies).

### Performance (output-identical)
- Build `BallisticInputs` once per integration instead of per derivative eval (eliminates per-step String alloc and Vec/DragTable clones on the Monte Carlo hot path).
- Binary-search the drag Mach axis; precompute WindSock segment vectors; hoist RK45 air-density/wind and per-step pitch-damping/drag-coefficient allocations; skip BC-segment re-sort when already sorted; lowercase the bullet model once.

## [0.13.30] - 2026-01-26

### Added
- **Terms of Service Acceptance** - When using `--online` for the first time, users must accept the Terms of Service from https://ballistics.rs/terms.txt
  - TOS is fetched from the server and displayed in the terminal
  - User must type 'y' or 'yes' to accept
  - Acceptance is stored in `~/.ballistics/tos.json`
  - Subsequent runs skip the prompt unless TOS version changes

### Dependencies
- Added `dirs` crate for cross-platform home directory detection

## [0.13.29] - 2026-01-26

### Added
- **Expanded Test Coverage** - Added 36 new unit tests to modules with lighter coverage:
  - `trajectory_integration`: RK4/RK45 consistency, ground impact detection, target distance, wind handling (+6 tests)
  - `fast_trajectory`: Solution interpolation edge cases, BC segment boundaries, event arrays (+6 tests)
  - `stability_advanced`: Bullet type parameters, atmospheric corrections, dynamic stability (+11 tests)
  - `spin_drift_advanced`: Drift direction, edge cases, transonic correction, yaw of repose (+13 tests)

### Changed
- Total test count increased from 156 to 192 tests

## [0.13.28] - 2026-01-26

### Fixed
- **Wind Direction CSV Override** - Fixed default check comparing against 90.0 instead of 0.0 (the actual CLI default), which prevented location CSV wind direction overrides from working

## [0.13.27] - 2026-01-26

### Fixed
- **Location CSV Overrides** - Fixed bug where `final_humidity` and `final_wind_direction` were set but not used, causing location CSV overrides for humidity and wind direction to be ignored
- **Compiler Warnings** - Eliminated all unused variable warnings in main.rs

### Changed
- Location CSV now properly overrides humidity (`HUMIDITY` column) and wind direction (`WIND_DIR` column)

**Related Tickets:**
- MBA-600: Fix unused variable warnings in ballistics-engine CLI

## [0.13.26] - 2026-01-26

### Added
- **Longitude Parameter** - `--longitude` for weather zone features (degrees, -180 to 180)
- **Shot Direction Parameter** - `--shot-direction` for azimuth (0=North, 90=East)

### Notes
- Negative values require equals format: `--longitude=-115.2`
- These parameters are required when using `--enable-weather-zones` or `--enable-3d-weather`

**Related Tickets:**
- MBA-601: Add weather integration control parameter for --online CLI mode

## [0.13.25] - 2026-01-26

### Added
- **Weather Zone Control** - `--enable-weather-zones` flag for automatic weather zone generation
- **3D Weather Control** - `--enable-3d-weather` flag for altitude-dependent atmospheric corrections
- **Wind Shear Model Selection** - `--wind-shear-model` parameter (none, logarithmic, power_law, ekman_spiral)
- **Weather Zone Interpolation** - `--weather-zone-interpolation` parameter (linear, cubic, step)

### Notes
- All weather parameters are passed through to Flask API when using `--online` mode
- Weather features require latitude, longitude, and shot-direction to be specified

**Related Tickets:**
- MBA-601: Add weather integration control parameter for --online CLI mode

## [0.13.24] - 2026-01-26

### Fixed
- **Online Mode Ground Impact** - Fixed `--ignore-ground-impact` flag not being passed to Flask API when using `--online` mode, causing trajectories to be truncated when bullet dropped below -100m threshold

### Added
- `ground_threshold` field in API client TrajectoryRequest

**Related Tickets:**
- Online solver returning partial results investigation

## [0.13.23] - 2026-01-25

### Added
- **BC Truing** - `--bc-adjustment` parameter for multiplying BC by a correction factor (e.g., 0.85)
- **Velocity Truing** - `--velocity-adjustment` parameter for adding offset to base velocity from chronograph data
- **CSV Profile Import** - `--profile` and `--profile-row` to load gun configurations from CSV files
- **CSV Location Import** - `--location` and `--site` to load shooting location environmental data from CSV files
- CSV parser handles Glenn's format (header with `#` prefix, comma-separated values)

### Changed
- `--velocity` and `--bc` are now optional when using `--profile` (loaded from CSV)
- Integration tests now use main `ballistics` binary instead of `ballistics-cli`

**Related Tickets:**
- MBA-591: CLI: Add --bc-adjustment parameter for BC truing
- MBA-592: CLI: Add --velocity-adjustment parameter
- MBA-593: CLI: Add CSV profile import (--profile)
- MBA-594: CLI: Add location presets (--location)

## [0.5.0] - 2025-01-31

### Added
- **Advanced Spin Drift Module** (`spin_drift_advanced.rs`) - Enhanced gyroscopic drift calculations with velocity-dependent effects (MBA-138)
- **Advanced Stability Module** (`stability_advanced.rs`) - Comprehensive stability analysis including transonic behavior (MBA-139)
- Both modules ported from private ballistics_rust repository as part of strategic reconciliation (MBA-144)

### Fixed
- **Critical Wind Vector Calculation** - Fixed swapped sin/cos in `wind.rs` `calc_vec()` function that caused 90° rotation of wind effects (MBA-146)
- **Wind Shear Bugs** - Multiple critical fixes in `wind_shear.rs` for proper altitude-dependent wind modeling (MBA-145)

### Verified
- **Transonic Drag Parity** - Verified parity with private repository implementation, all public functions available (MBA-140)

### Context
This release represents significant progress in the strategic reconciliation between the private ballistics_rust and public ballistics-engine repositories. The strategy established in MBA-144 v0.30.0 makes ballistics-engine the canonical physics source, with improvements flowing from private → public → wrapped in private.

**Module Reconciliation Progress:**
- 7 modules wrapped in ballistics_rust using ballistics-engine
- 2,164 lines of duplicate code eliminated (25.5% reduction)
- ballistics_rust now depends on ballistics-engine as authoritative source

**Related Tickets:**
- MBA-144: Strategic reconciliation with ballistics-engine as canonical source
- MBA-138: Port spin_drift_advanced.rs
- MBA-139: Port stability_advanced.rs
- MBA-140: Verify transonic_drag.rs parity
- MBA-142: Align Rust dependencies (nalgebra 0.33, rayon 1.10)
- MBA-145: Fix critical wind_shear.rs bugs
- MBA-146: Wind vector calculation bug
- MBA-147: Reynolds.rs missing FlowRegime enum

## [0.4.3] - 2025-01-20

### Fixed
- Fixed metric unit calculations
- Updated WASM to use RK45 integration method

## [0.4.0] - 2025-01-15

### Added
- RK45 integration method as default (improved accuracy)
- Muzzle height and target height parameters for realistic trajectory simulation

### Changed
- Major physics improvements across trajectory calculations

## [0.3.0] - 2024-12-01

### Added
- Initial public release
- Core trajectory physics
- RK4 and Euler integration methods
- Monte Carlo simulation support
- WASM support for web deployment

[0.5.0]: https://github.com/ajokela/ballistics-engine/compare/v0.4.3...v0.5.0
[0.4.3]: https://github.com/ajokela/ballistics-engine/compare/v0.4.0...v0.4.3
[0.4.0]: https://github.com/ajokela/ballistics-engine/compare/v0.3.0...v0.4.0
[0.3.0]: https://github.com/ajokela/ballistics-engine/releases/tag/v0.3.0
