# ADR-0006 — Append-only JSONL as source of truth, SQLite as derived index

**Status:** accepted · **Date:** 2026-07-26

## Context

Agent runs cost real money. Re-running to change an alpha, a margin, or a plot is unacceptable.
Requirements: re-analyze without re-running, memoize trials across runs, resume after a crash,
and publish a log a skeptic can recompute from.

## Decision

- **`runs/<run_id>/trials.jsonl`** — one JSON object per trial, appended, never mutated. This is
  the source of truth.
- **`index.db`** (SQLite) — `(task_id, variant_hash, agent, model, replicate) → (run_id, offset)`
  plus denormalized filter columns. Fully rebuildable; deleting it is never data loss.
- **`traces/<trial_id>.jsonl.gz`** — raw agent event streams, referenced by path, not inlined.

## Rejected: SQLite as the source of truth

Cleaner queries, transactional, one file. Rejected because:
- **Publishing.** The launch artifact ([10-distribution.md](../10-distribution.md)) requires
  handing a reader a file they can grep, diff, and load in any language. A binary DB is a worse
  artifact for the one use case where the data format is user-facing.
- **Append-only-ness gives crash resumption for free.** With a DB you handle partial writes and
  locking across concurrent trial workers; with `O_APPEND` line writes you do not.
- Concurrent writers from a thread pool into SQLite means WAL mode and lock contention for no
  benefit at this scale.

## Rejected: JSONL only, no index

Simplest. Rejected because trial memoization is the feature that makes iterative use affordable
([06-cost.md](../06-cost.md)), and it requires a fast lookup by cache key across *all* prior runs.
Linear-scanning every historical JSONL on every trial is fine at 80 trials and bad at 5,000.
SQLite is stdlib; there is no dependency cost.

## Rejected: a hosted results service

Would enable cross-user task-suite sharing and comparison. Explicitly out of scope
([02-scope.md](../02-scope.md)) — the moment there's a server there's a second product, an
account system, and a privacy story for people's repo contents.

## Consequences

- Every statistical parameter (α, margin, bootstrap count, metric choice, degenerate-task
  exclusion) is a **post-hoc, free decision**. This is the single most cost-relevant design
  choice in the project.
- It also makes p-hacking easy: a user can re-analyze until they like the answer. Mitigation is
  visibility, not prevention — the report footer records how many analyses have been run against
  a dataset and whether a run extends an earlier one
  ([05-statistics.md](../05-statistics.md)).
- Schema evolution needs a `schema_version` field on every line and a reader that tolerates
  unknown fields, or old runs become unreadable after the first refactor.
- Traces are large. Need a retention policy (`ablate clean --traces --older-than 30d`) or users
  will find gigabytes in their cache dir and be annoyed.
