# `Threadline.Retention.Policy`
[🔗](https://github.com/szTheory/threadline/blob/v0.9.0/lib/threadline/retention/policy.ex#L1)

Validates **`config :threadline, :retention`** before purge runs.

v1.3 exposes a **single global retention window** (`:keep_days` or
`:max_age_seconds`, mutually exclusive) plus an **`enabled`** flag that must
be true for destructive purge. **`delete_empty_transactions`** defaults to
`true` (remove parent `audit_transactions` rows with no remaining children
after change deletes).

Per-table / per-tenant overrides are **not** in v1.3; callers should treat
this module as the guardrail for the global policy shape only.

# `t`

```elixir
@type t() :: %Threadline.Retention.Policy{
  delete_empty_transactions: boolean(),
  enabled: boolean(),
  window_seconds: pos_integer()
}
```

Normalized retention options as returned by `resolve/1`.

# `cutoff_utc_datetime_usec!`

```elixir
@spec cutoff_utc_datetime_usec!(keyword()) :: DateTime.t()
```

Returns UTC `DateTime` strictly **before** which `AuditChange.captured_at` values
are considered expired for purge (i.e. delete rows with `captured_at < cutoff`).

Uses `DateTime.add/3` in microsecond mode for consistency with `:utc_datetime_usec`.

# `resolve!`

```elixir
@spec resolve!(keyword() | map()) :: t()
```

Resolves config into a struct or raises like `validate_config!/1`.

# `validate_config!`

```elixir
@spec validate_config!(keyword() | map()) :: :ok
```

Validates retention config from `Application.get_env(:threadline, :retention)`.

Raises `ArgumentError` with a message containing `"retention"` when the shape
is invalid, keys conflict, or the window is not positive.

In `:test`, missing `:keep_days` / `:max_age_seconds` is allowed only when the
caller passes a non-empty map/list that still fails other checks — for empty
config in test, hosts should set explicit values in `config/test.exs`.

---

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