|
| 1 | +# ADR 0018: trend history is append-only JSONL; alerts stay exit-code-first |
| 2 | + |
| 3 | +- Status: accepted |
| 4 | +- Date: 2026-07-04 |
| 5 | + |
| 6 | +## Context |
| 7 | + |
| 8 | +M14 gives the drift sentinel (ADR-0017) a memory and a voice: reliability-over-time for the |
| 9 | +dashboard/CLI, and a push alert. The history format is a data contract (the API, dashboard, and |
| 10 | +users' tooling read it), so it deserves a recorded decision. |
| 11 | + |
| 12 | +## Decision |
| 13 | + |
| 14 | +1. **History is append-only JSONL** (`./.volo/shadow-history.jsonl`): one |
| 15 | + `{at, snapshot, drift}` line per `volo shadow check`, including the baseline-establishing |
| 16 | + run (`drift: null`). Torn lines are skipped on read, so a crashed run can't poison the |
| 17 | + history. Committable and greppable, consistent with ADR-0017's no-database stance. |
| 18 | +2. **Two derived series, computed on read:** `fleet_series` (each dimension averaged across all |
| 19 | + banked traces, per check — the dashboard headline) and `trace_series(run_id)` (one banked |
| 20 | + trace over time). Nothing is precomputed or stored twice. |
| 21 | +3. **Alerting stays exit-code-first.** The webhook (`--webhook` / `VOLO_SHADOW_WEBHOOK`) is a |
| 22 | + loud *secondary* path: a Slack-compatible payload (`text` headline + full report under |
| 23 | + `volo`), delivered best-effort — a dead webhook logs a warning and never masks the exit-3 |
| 24 | + alert. Stdlib `urllib` only; no HTTP dependency. |
| 25 | +4. **Dashboard/API surface:** `GET /shadow/history` returns `{checks: fleet_series, corpus: |
| 26 | + bank inventory}`; `GET /shadow/history/{run_id}` returns one trace's series. The `/shadow` |
| 27 | + web screen renders fleet-average sparklines per dimension plus drifted-night chips, reusing |
| 28 | + the CI sparkline component. |
| 29 | + |
| 30 | +## Consequences |
| 31 | + |
| 32 | +- The history file grows without bound (~1–2 KB per check per 10 traces); rotation/compaction |
| 33 | + is deliberately deferred until a real corpus shows the growth rate. Append-only means |
| 34 | + rotation is a safe external `mv`. |
| 35 | +- Averaging the fleet hides a single trace regressing among many healthy ones in the *chart*; |
| 36 | + the alert does not average — `compare` runs per trace, so the exit code still fires. The |
| 37 | + per-trace series exists for the drill-down. |
| 38 | +- Slack compatibility via `text` keeps zero config for the most common webhook; consumers |
| 39 | + needing richer formats read the `volo` key. |
| 40 | + |
| 41 | +## Alternatives considered |
| 42 | + |
| 43 | +- **SQLite for history** — rejected for now: the query needs (append, scan) fit JSONL; SQLite |
| 44 | + adds locking/migration surface for no current query we can't do in one pass. |
| 45 | +- **Alert-only-webhook (no exit code change)** — rejected: CI schedulers act on exit codes; |
| 46 | + webhooks fail silently. |
| 47 | +- **Storing derived series** — rejected: two sources of truth; recompute is O(history) and fast. |
0 commit comments