## 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`: отдельной задачи прогона не заводим, второго проигрывателя в проекте не остаётся.