# `Sidereon.GNSS.Staleness`
[🔗](https://github.com/neilberkman/sidereon-ex/blob/main/lib/sidereon/gnss/staleness.ex#L1)

Product-staleness graceful degradation for time-varying GNSS products.

Time-varying products (IONEX vertical-TEC maps, rapid/predicted SP3 orbit and
clock files) publish with latency and gaps, so the product for the exact
requested epoch is not always on hand. This is the Elixir surface over the
`sidereon-core` selection layer: given a SET of already-parsed products and a
requested epoch (or epoch range), it returns a usable product plus a
`Sidereon.GNSS.Staleness.StalenessMetadata` describing which source epoch was
used and how stale it is, falling back to the most-recent product within a
configurable staleness cap. A request that would rely on a product older than
the cap fails with a typed error instead of returning data past the cap, so a
degraded answer is never substituted silently.

This layer is pure and does no networking: it selects among products the
caller has already parsed (`Sidereon.GNSS.SP3.load/1`,
`Sidereon.GNSS.Ionosphere.load_ionex/1`). Fetching the products is a separate,
per-binding concern.

## Degradation paths

  * `:exact` - a product covers the requested epoch; it is returned untouched,
    so the downstream evaluation is bit-for-bit identical to querying it
    directly. Staleness is zero.
  * `:nearest_prior` (SP3) - no product covers the epoch, so the most-recent
    prior product is used as-is, with staleness measured from its last epoch.
  * `:diurnal_shift` (IONEX) - no product covers the requested day, so a prior
    day's grid is advanced by whole days onto the requested epoch (TEC is
    approximately 24-hour periodic). Only the epoch axis moves; grid values are
    unchanged.

## Epochs

Epochs are a `NaiveDateTime` or `{{y, m, d}, {h, min, s}}` tuple, interpreted
in the product's own time scale (no leap-second shifting), and converted to
seconds since J2000 via `Sidereon.GNSS.Time`. The IONEX map-epoch axis is
integer seconds, so an IONEX request must be a whole-second epoch.

# `epoch`

```elixir
@type epoch() :: NaiveDateTime.t() | tuple()
```

An epoch as a `NaiveDateTime` or `{{y, m, d}, {h, min, s}}` tuple.

# `selection_error`

```elixir
@type selection_error() ::
  :empty_product_set
  | {:invalid_range, float(), float()}
  | {:no_prior_product, float()}
  | {:beyond_staleness_cap, map()}
  | {:invalid_product, String.t()}
  | {:invalid_policy, float()}
  | {:overflow, String.t()}
```

A typed selection failure. `info` for `:beyond_staleness_cap` is a map with the
requested/source epochs, the staleness, and the cap, all in J2000 seconds.

# `select_ionex`

```elixir
@spec select_ionex([reference()], epoch(), Sidereon.GNSS.Staleness.Policy.t()) ::
  {:ok, Sidereon.GNSS.Staleness.IonexSelection.t()} | {:error, term()}
```

Select an IONEX product usable at `epoch`, degrading to a diurnal-shifted prior
product within `policy`.

`handles` is a list of parsed-IONEX references from
`Sidereon.GNSS.Ionosphere.parse_ionex/1` or `load_ionex/1`. The IONEX axis is
integer seconds, so `epoch` must be a whole-second epoch. Returns
`{:ok, %Sidereon.GNSS.Staleness.IonexSelection{}}` or `{:error, reason}`.

# `select_ionex_over_range`

```elixir
@spec select_ionex_over_range(
  [reference()],
  epoch(),
  epoch(),
  Sidereon.GNSS.Staleness.Policy.t()
) ::
  {:ok, Sidereon.GNSS.Staleness.IonexSelection.t()} | {:error, term()}
```

Select an IONEX product usable across `[start_epoch, end_epoch]`.

See `select_ionex/3`; this is the range case.

# `select_sp3`

```elixir
@spec select_sp3([Sidereon.GNSS.SP3.t()], epoch(), Sidereon.GNSS.Staleness.Policy.t()) ::
  {:ok, Sidereon.GNSS.Staleness.Sp3Selection.t()} | {:error, term()}
```

Select the SP3 product to use for `epoch`, degrading to the most-recent prior
product within `policy`.

An `:exact` result covers `epoch`; a `:nearest_prior` result is the best
in-cap candidate and may end before `epoch` (its coverage gap is the reported
staleness), so a downstream solve against it can still fail to serve `epoch`.

`products` is a list of parsed `Sidereon.GNSS.SP3` products. Returns
`{:ok, %Sidereon.GNSS.Staleness.Sp3Selection{}}` or `{:error, reason}`.

# `select_sp3_over_range`

```elixir
@spec select_sp3_over_range(
  [Sidereon.GNSS.SP3.t()],
  epoch(),
  epoch(),
  Sidereon.GNSS.Staleness.Policy.t()
) :: {:ok, Sidereon.GNSS.Staleness.Sp3Selection.t()} | {:error, term()}
```

Select an SP3 product usable across `[start_epoch, end_epoch]`.

See `select_sp3/3`; this is the range case, where staleness is measured to the
range end (the most-stale point).

---

*Consult [api-reference.md](api-reference.md) for complete listing*
