# `Raxol.Core.Telemetry.Invariants`
[🔗](https://github.com/DROOdotFOO/raxol/blob/v2.7.0/lib/raxol/core/telemetry/invariants.ex#L1)

Declares the `:telemetry` events a package emits, each classified as exactly
one of `:invariant`, `:peer` or `:operational`, and provides the source
scanner that keeps such a declaration honest.

## Why this exists

A telemetry event is only a guard if something fails when it fires.
`Raxol.AgentClientProtocol.Session` detected a turn that produced zero
`session/update` frames for a non-empty prompt, logged a warning AND emitted
an event -- and the bug shipped anyway, because no test asserted on the
event and the package did not even depend on `:telemetry`, so the emit site's
`Code.ensure_loaded?/1` guard was permanently false. Classification turns
"the system noticed and told nobody" into "the suite fails":

  * `:invariant` -- can only fire if Raxol itself is wrong. Enforced:
    `Raxol.Core.Telemetry.InvariantSentinel` fails any test in which one
    fires unless the test declares it with `@tag expect_invariant:`.
  * `:peer` -- caused by a remote party misbehaving (a chain RPC, a chat
    API, a peer agent, a malformed inbound frame). Not enforced.
  * `:operational` -- normal life: cache hits, policy decisions, lifecycle,
    retries, user API misuse, filesystem and clock reality. Not enforced.

The criterion, applied literally: **if a peer, the network, the chain, the
filesystem, the clock, or a user can cause it, it is NOT an invariant.** Err
toward `:peer`/`:operational`. A false invariant makes the suite flaky, and a
flaky guard gets deleted, which is strictly worse than a missed one.

## Why this module lives in `lib/` and not `test/support/`

Dependent packages `use` this from THEIR test files, and a dependency's
`test/support` tree is not on a consumer's load path -- only its `lib/` is
compiled into the delivered application. Nothing here requires ExUnit at
runtime: this module never touches it, and
`Raxol.Core.Telemetry.InvariantSentinel` touches it only inside functions it
injects into (or is called from) a test module.

## Usage

    defmodule Raxol.Payments.Telemetry do
      use Raxol.Core.Telemetry.Invariants,
        events: %{
          [:raxol, :payments, :xochi, :unchecked_settlement] => :invariant,
          [:raxol, :payments, :spend] => :operational
        },
        dynamic: [[:raxol, :earn, :buyer, :queue]]
    end

That injects `events/0`, `invariant_events/0`, `dynamic_families/0` and
`classification/1`.

`events:` must be written as a literal map of literal event names, because
the registry's whole value is being readable from source: a computed map
could not be checked at compile time, and a duplicate key in a computed map
would silently collapse rather than raise.

## Dynamic families

Some events build their final segment at runtime
(`[:raxol, :earn, :buyer, :queue, suffix]`). A source scan cannot enumerate
those names, so the family prefix is declared in `dynamic:` instead. An entry
is either a bare family (classified `:operational`) or a
`{family, classification}` pair. A dynamic family may NOT be `:invariant`:
the sentinel cannot subscribe to a name it cannot spell.

## Completeness is asserted, not trusted

`scan_lib!/1` parses a package's `lib/` for the event literals actually
passed to an emit call, so a per-package registry test can fail when a new
event is added without anyone classifying it. That test is the load-bearing
half of the mechanism.

# `classification`

```elixir
@type classification() :: :invariant | :peer | :operational
```

How a fired event should be read.

# `event`

```elixir
@type event() :: [atom()]
```

A telemetry event name, or (for a dynamic family) its prefix.

# `classify`

```elixir
@spec classify(event(), %{required(event()) =&gt; classification()}, [
  {event(), classification()}
]) ::
  classification() | nil
```

Resolve `event` against a static registry map and a list of
`{family, classification}` dynamic entries.

Exact matches win; otherwise the longest matching family prefix does, so a
narrower family can refine a broader one.

# `scan_lib!`

```elixir
@spec scan_lib!(Path.t()) :: [event()]
```

Parse every `.ex` file under `lib_path` and return the sorted unique
`[:raxol, ...]` event literals that appear as the FIRST ARGUMENT of an emit
call.

An emit call is any call -- local, or qualified by any module -- named
`execute`, `span`, `emit_telemetry` or `emit_event` whose first argument is a
literal `[:raxol, ...]` list of atoms. The rule is deliberately
module-agnostic: this repo emits through `:telemetry.execute/3` directly, and
also through wrappers (`Raxol.Core.Telemetry.TraceContext.execute/3`, a
package's own `Telemetry.span/3`) that a `:telemetry`-only rule would miss,
leaving a correctly classified event looking like a phantom registry row.
In the other direction, `:telemetry.attach_many/4` takes event names too, but
subscribing is not emitting, and a registry that demanded classification of
events nobody emits would be noise that gets the completeness test disabled.

A first argument given as a module attribute (`@telemetry_event
[:raxol, ...]` then `:telemetry.execute(@telemetry_event, ...)`) is resolved
against the literal attribute definitions in the same file.

The scan is AST-based on purpose: an event named in a moduledoc or a comment
is prose, not an emit site. Event names assembled at runtime
(`[:raxol, :earn, :buyer, :queue, suffix]`) are not literals, so they never
appear here -- declare them as `dynamic:` families instead.

Raises when `lib_path` is not a directory or holds no `.ex` files, so a moved
source tree fails loudly instead of letting a completeness test pass on an
empty scan.

---

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