# The warning performance ledger

**Since:** migration 215 (written 2026-10-29). **Table:** `warning_verifications`. **Writer:** `/api/workers/warning-ledger`, hourly. **Reader:** `GET /api/v1/verification/tornado-warnings?month=YYYY-MM` (public). **Logic:** `lib/field-reports/ledger.ts` over `lib/field-reports/tornado-warnings.ts`, unchanged.

## What it is

Each NWS-surveyed tornado track (Damage Assessment Toolkit, in `observed_footprints`) set against the NWS warning in effect at its touchdown point, stored as a row that can be cited: the verdict (`warned`, `severe_thunderstorm_only`, `unwarned`), the lead time, the covering warning with its issue and expiry, the forecast office on both sides, the EF rating and casualties as surveyed, and the 2020 housing under the warning polygon.

It is the third item of the data-asset program (`docs/as-issued-archive.md` is the first; `docs/hindcast/` the second): the public tornado field report already reaches these verdicts, but per request, storing nothing. A verdict that cannot be re-read a year on, broken out by office, and weighted by what was under the warning is a page, not a record.

## What a row says — and does not

- **The verdict is the field report's.** `verifyTrack` in `tornado-warnings.ts` decides; the ledger never does. If that logic changes, `LEDGER_METHOD_VERSION` changes, the worker writes new rows under the new version, and the old rows stay.
- **`lead_minutes` runs from the warning's onset/effective time**, which is what the platform stores as the warning's start — not from the time the alert was sent. NWS verification statistics use issuance; this will read a little shorter.
- **Containment is the touchdown point.** A warning that covered the end of a long track but not its start is "not in effect at touchdown".
- **`office` is the survey's** (DAT `office`, e.g. `OUN`); **`warning_office` is the issuer's**, from the alert's CAP `senderName` in the as-issued archive (`as_issued_products`, source `nws_alerts`), and is null for alerts issued before the archive began on 2026-10-28 or that it does not hold. This is the first consumer of the archive.
- **Housing** is `compute_housing_exposure_v2` over the warning polygon: 2020 tracts weighted by developed land. A summary counts each warning once, however many tracks it covered.
- **Surveys are revised for weeks.** The worker recomputes the last 45 days every hour and rewrites each outcome's row (`computed_at` says when). The ledger is not append-only; the archive underneath it is.
- **The record begins 2026-10-09**, when the survey ingest began. 33 tracks as of 2026-10-29. Nothing is backfilled from any other archive yet.

## What it is not

- Not NWS verification. The NWS computes probability of detection, false-alarm ratio and lead time per office from its own records with issuance times and full-path checks. This ledger scores only surveyed tornadoes (no false-alarm side yet: an unverified warning is not a row) and is a different measure, not a reproduction.
- Not multi-peril yet. Flash flood warnings against USGS high-water marks need a peak time per mark, which STN does not publish; hurricane watches against the best-track swath need a per-zone product. Both are designed for in the schema (`peril`) and not built.
- Not a backtest — but one exists beside it: `docs/ledger-backtest/` replays the same question over 2020–2025 from the Iowa Environmental Mesonet's warning polygon archive and the DAT service's own history (6,397 tornado-rated surveys; 56.1% warned at touchdown, median lead 13 minutes, EF2+ 73.7%), under its own method version (`tornado-iem-1`, `lib/field-reports/backtest-iem.ts`), with lead from first issuance rather than onset. The two are not merged.

## Verifying a row

```sql
SELECT observed_at, office, verdict, lead_minutes, warning_office, warning_housing_units, computed_at
FROM warning_verifications
WHERE peril = 'tornado' AND method_version = 'tornado-1'
ORDER BY observed_at DESC;
```

The same month on the public route: `/api/v1/verification/tornado-warnings?month=2026-10`.
