## 1. Опоры в существующих пакетах

- [x] 1.1 `internal/ident`: `TimeOf(id string) (time.Time, error)` — время
      создания из ULID, **в UTC**, усечённое до секунды (как `store.Now`).
      `ulid.Time` внутри зовёт `time.Unix` и отдаёт локальную зону, а
      `Truncate` зону не нормализует — значит `.UTC()` обязателен явно.
- [x] 1.2 `internal/archive`: перечисление тел — обход корня, отбор форм тела,
      возврат относительных путей и отдельно — пропущенных файлов. Ошибка
      чтения каталога (включая отсутствие корня) возвращается наружу, а не
      превращается в пустой список; каталог не создаётся.
- [x] 1.3 `internal/store`: `ListDeliveries(ctx)` — доставки в порядке
      `(received_at, id)` со **всеми фактами журнала**, включая `headers`.
- [x] 1.4 `internal/store`: открытие рабочей базы **только для чтения и без
      наката миграций** + проверка версии схемы; расхождение — ошибка,
      называющая обе версии.

## 2. Проигрывание журнала — `internal/replay`

- [x] 2.1 Собрать вход: объединить тела архива с учётом доставок; развести
      четыре случая (штатная / тело без учёта / учёт без тела / файл не тело) и
      отсортировать по `(received_at, id)`.
- [x] 2.2 Перенести учёт в базу назначения по нормированному составу: факты
      журнала дословно (включая `headers`), производные от разбора —
      пустыми (`parse_status=pending`, `points=0`, `derived_layer=''`,
      `uncovered_sections='[]'`). Подобранные тела завести заново: id из имени
      файла, метка из ULID в UTC, `bytes`/`sha256` — по **распакованному** телу.
- [x] 2.3 Проиграть журнал последовательно: `fold.Fold` по каждой доставке,
      `fold.New` собирается с тем же пределом тела, что и приём
      (`cfg.Ingest.MaxBodyMB`). Отказ одной доставки не прекращает прогон,
      отмена контекста — прекращает.
- [x] 2.4 Собрать отчёт данными: счётчики по классам, отпечаток пересобранной
      витрины, признак отмены.

## 3. Команда `healthlog reindex`

- [x] 3.1 `cmd/healthlog/reindex.go`: флаги `--config`, `--out`, `--force`;
      умолчание `--out` — сосед рабочей базы; тождество с рабочей базой
      проверяется **по файлу** (`os.SameFile`), а для несуществующего файла —
      по родительскому каталогу и имени; `flag.ErrHelp` не превращается в
      `fatal`.
- [x] 3.2 Жизненный цикл файла назначения: сборка под временным именем,
      переименование — последний шаг успеха; при любом ином исходе файла по
      пути назначения нет, временный и его спутники (`-wal`, `-shm`) убраны.
- [x] 3.3 Отмена: `signal.NotifyContext(SIGINT, SIGTERM)`, частичный отчёт,
      ненулевой код.
- [x] 3.4 `func writeReport(w io.Writer, …)` — отчёт в stdout, прогресс в
      stderr; процедура подмены печатается только при успешном исходе.
- [x] 3.5 Коды возврата: успех — журнал непуст и свёрнута хотя бы одна
      доставка; ненулевой — пустой журнал, ноль свёрнутых, отмена, ошибка
      окружения. Отказ отдельной доставки исхода команды не меняет.
- [x] 3.6 Подключить подкоманду в `main.go`.

## 4. Проверки

- [x] 4.1 Сходимость: живой приём N доставок → пересборка в отдельную базу →
      отпечатки совпали; второй прогон → отпечаток не изменился.
- [x] 4.2 Порядок: журнал, у которого раскладка файлов расходится с
      хронологией, даёт доставке без плотных метрик слой **предшествующей** по
      `received_at`, а не соседа по каталогу; доставки одной секунды упорядочены
      идентификатором.
- [x] 4.3 Состав журнала: тело без учётной записи подбирается, точки доезжают,
      `bytes`/`sha256` посчитаны по распакованному телу; учётная запись без тела
      считается и не роняет прогон; файл не-тело считается отдельно; нечитаемый
      каталог — отказ команды.
- [x] 4.4 Учёт в базе назначения: число строк и `headers` совпадают с рабочей
      плюс подобранные; проставленный в рабочей базе `derived_layer` на
      результат не влияет (отпечаток тот же, что из учёта без слоёв).
- [x] 4.5 Отказы и обратимость: битое тело не срывает прогон; отмена прекращает
      проигрывание и не оставляет файла назначения; рабочая база после
      прерванной пересборки не изменилась ни витриной, ни учётом.
- [x] 4.6 Аргументы: `--out` — тот же файл, что рабочая база, по другому пути;
      существующий файл без `--force`; `--force` даёт ту же витрину, что сборка
      в отсутствующий файл.
- [x] 4.7 Исход команды: пустой журнал — ненулевой код и без процедуры подмены;
      ноль свёрнутых — то же.
- [x] 4.8 Отчёт не несёт значений точек, имён метрик и имён устройств.
- [x] 4.9 Прогон живого архива переписан поверх `replay` (см. 6.5), измеренные
      утверждения сохранены.

## 5. Приёмочные критерии из ревью дизайна

Рубрика порождена проходом `healthlog-review-rubric` до чтения предложения.
Пункты, уже закрытые разделами выше, отмечены ссылкой.

- [x] 5.1 **Идемпотентность прогона.** Второй прогон даёт то же состояние не
      только по отпечатку витрины, но и по учёту доставок в базе назначения
      (число строк, `id`, `received_at`, `bytes`, `sha256` подобранных тел).
- [x] 5.2 **Тотальный детерминированный порядок.** Результат не зависит от
      порядка обхода каталога, часового пояса процесса и числа перечитываний
      каталога (см. 4.2).
- [x] 5.3 **Независимость от «сейчас».** Ни одно поле, влияющее на отпечаток, не
      производно от времени прогона: метки берутся из события, а не из
      `store.Now`. Проигрывание последовательно.
- [x] 5.4 **Незавершённая сборка неотличимой от завершённой быть не может**
      (см. 3.2, 4.5).
- [x] 5.5 **Отмена доводится до конца и различима снаружи** (см. 3.3).
- [x] 5.6 **Оракул успеха не сводится к «ошибок не было»** (см. 3.5, 4.7).
- [x] 5.7 **Политика частичного отказа явная и счётная.** У каждого класса
      отказа свой счётчик, сумма счётчиков сходится с числом входов.
- [x] 5.8 **Рабочая база открывается только на чтение и без миграций**
      (см. 1.4).
- [x] 5.9 **Расход памяти не растёт с объёмом архива.** Тела читаются по
      одному, предел распакованного тела тот же, что у приёма (см. 2.3);
      превышение — учтённый отказ доставки, а не падение прогона.
- [x] 5.10 **Длинный прогон наблюдаем** (см. 3.4).
- [x] 5.11 **Аргументы безопасны и обратимы** (см. 3.1, 4.6).
- [x] 5.12 **Невосстановимое названо, а не досчитано.** Отчёт называет классы
      ожидаемого расхождения (новые доставки за время прогона, непереносимый
      `sealed`, исправленный разбор) отдельно от самого факта расхождения.
- [x] 5.13 **Ни отчёт, ни лог выше `DEBUG` не несут данных о здоровье**
      (см. 4.8).

## 6. Документация

- [x] 6.1 `docs/architecture.md`: почему подмена базы не автоматизируется
      (открытый дескриптор, порча базы SQLite) и почему пересборка идёт в
      отдельный файл (blue-green rebuild проекции); отвергнутые варианты — с
      причиной. Там же — правка утверждения «пересчёт при `reindex` идёт по всей
      истории и потому точнее»: точнее не длина ряда, а исправленный разбор.
- [x] 6.2 `README.md`: `reindex` перестаёт быть «в планах», процедура применения.
- [x] 6.3 `docs/plan.md`: `reindex` вычеркнут из остатка шага «Разбор и
      хранилище».
- [x] 6.4 Беклог: задача удалена, заведена новая — «заголовки доставки в архиве
      рядом с телом» (prior art: WARC), с указанием, что без неё пересборка без
      рабочей базы деградирует по выводу слоя.
- [x] 6.5 `task verify:archive` переезжает на `internal/replay`: отдельной
      задачи прогона не заводим, второго проигрывателя в проекте не остаётся.
