# `Foresight.Evals.DegradationWatch`
[🔗](https://github.com/fosferon/foresight/blob/v0.3.2/lib/foresight/evals/degradation_watch.ex#L1)

Counts the ways a retain pipeline silently degrades, so an eval can refuse to score a
run that was not actually measuring what it claims.

Both degradations are deliberate resilience behaviours: extraction falls back to storing
raw content when the LLM fails, and consolidation skips a scope after exhausting its
retries. Retain still succeeds in both cases, which is right for production and wrong
for a benchmark — the arm quietly gets worse while everything reports healthy.

This is not hypothetical. On 2026-08-14 a head-to-head ran with 10 extraction
fallbacks and 5 skipped merges: the Foresight arm was storing raw blobs instead of
extracted facts, which is precisely the deficit worth -0.267 answer accuracy (measured in an
earlier retain-extraction calibration). The harness would have printed a confident scoreboard had it not
independently run out of wall-clock, and that number would have entered the record.

Log lines already existed for both. A log line is not a guard, because nothing can
assert on it.

# `t`

```elixir
@type t() :: %{
  extraction_fallbacks: non_neg_integer(),
  merges_skipped: non_neg_integer()
}
```

# `clean?`

```elixir
@spec clean?(t()) :: boolean()
```

True when nothing degraded — the only state in which a run may be scored.

# `events`

```elixir
@spec events() :: [[atom()]]
```

Telemetry events this watches. Public so a test can assert the emitters use them.

# `start`

```elixir
@spec start() :: term()
```

Begin counting. Returns a handle for `stop/1`.

Counters are `:atomics`-backed because the eval fans instances out across tasks, so
increments arrive from many processes at once.

# `stop`

```elixir
@spec stop(term()) :: t()
```

Stop counting and return the totals.

---

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