Foresight.Evals.DegradationWatch (Foresight v0.1.0)

Copy Markdown View Source

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 (prior calibration, Cluster A). 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.

Summary

Functions

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

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

Begin counting. Returns a handle for stop/1.

Stop counting and return the totals.

Types

t()

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

Functions

clean?(arg1)

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

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

events()

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

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

start()

@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(arg)

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

Stop counting and return the totals.