## 1. Опоры в хранилище - [x] 1.1 Миграция `00006`: частичный индекс `delivery (received_at, id) WHERE parse_status = 'pending'`. Комментарий объясняет, почему частичный, а не по `parse_status`: в установившемся режиме в нём ноль–одна строка, полный хранил бы всю таблицу ради выборки из одной. - [x] 1.2 `store.PendingDeliveries(ctx, after, limit)` — неразобранные доставки в порядке `(received_at, id)`, строго после курсора; отдаёт идентификатор и `received_at`. Сравнение курсора — **row-value** `(received_at, id) > (?, ?)`: развёрнутая форма через `OR` даёт `SCAN` вместо `SEARCH` (проверено `EXPLAIN QUERY PLAN`). Нулевой курсор — нулевое время и пустой идентификатор, без ветки «первая страница». - [x] 1.3 `store.CountPendingDeliveries(ctx)` — размер задолженности для строки `INFO` при старте. - [x] 1.4 `store.ErrBusy` — доменный сентинел занятости базы; `inTx` оборачивает им исчерпание повторов, чтобы вызывающий не разбирал коды драйвера. - [x] 1.5 Обновить `docs/database.md`: новый индекс в перечне индексов `delivery`. ## 2. Исход свёртки отражает доставку, а не обстоятельства - [x] 2.1 `fold.fail`: отмена контекста и `store.ErrBusy` статус **не меняют** — доставка остаётся `pending`; всё прочее по-прежнему `failed`. Уровень лога по адресату: занятость и отмена — `WARN` (пройдёт само), остальное как сейчас. - [x] 2.2 Тест: свёртка на занятой базе оставляет доставку `pending`; свёртка непонятого содержимого оставляет `failed`. ## 3. Общий проигрыватель — `internal/replay` - [x] 3.1 `replay.Player.Play(ctx, deliveryID) (Outcome, error)` — свернуть одну доставку и вернуть её исход **значением**: ровно один классовый счётчик равен единице, плюс `partial`/`incomparable` у успешной свёртки. Классифицируется **только ошибка**; на контекст `Player` не смотрит — решение «нас остановили» принимает цикл. - [x] 3.2 `replay.Outcome` + `(*Outcome).Add(other)`; `replay.Report` встраивает `Outcome`, чтобы имена исходов не раздвоились. Существующие вызывающие (`cmd/healthlog/reindex*.go`, тесты) читают поля по-прежнему. - [x] 3.3 `replay.Run` переводится на `Player`, свою ветку отмены оставляет себе. Поведение и отчёт не меняются — проверяется существующими тестами пакета. - [x] 3.4 Табличный тест классификатора: ошибка → ожидаемый `Outcome`. ## 4. Воркер свёртки - [x] 4.1 `replay.Worker` с синхронным швом: `Pass(ctx) (Outcome, error)` — один проход, без каналов; `Run(ctx)` — тонкий `select` поверх него по сигналу, тику и отмене; `Notify()` — неблокирующая отправка в канал ёмкостью 1; `done` закрывается в `defer` внутри `Run`. - [x] 4.2 `Pass`: выбирать `pending` порциями по курсору, сворачивать через `Player`, курсор строго возрастает; отмена проверяется **между** доставками; проход конечен даже когда доставка осталась `pending`. - [x] 4.3 Свёртка внутри прохода идёт на `context.WithoutCancel` от контекста прохода плюс собственный дедлайн (`foldTimeout`, переезжает из `internal/ingest`). - [x] 4.4 Отказ выборки — `ERROR` и выход из `Pass`, но не из `Run`: воркер переживает временный отказ базы. - [x] 4.5 Наблюдаемость: `INFO` с размером задолженности перед первым проходом; `WARN` «доставка ждала свёртки дольше пяти минут» — считается на выборке прохода, включается после того, как первый проход завершился. Ни значений точек, ни имён устройств. ## 5. Приём без свёртки - [x] 5.1 `ingest.Service` теряет зависимость от `fold`: `Accept` пишет тело, учитывает доставку, логирует принятие и будит воркер. `foldTimeout` и вызов свёртки уходят. - [x] 5.2 Учёт доставки — на `context.WithoutCancel` с коротким дедлайном: обрыв соединения после записи тела не должен оставлять тело без строки в журнале. Проверка формы тела остаётся на исходном контексте. - [x] 5.3 Сигнал воркеру — параметр конструктора функцией; `nil` приводится к пустой функции **один раз в конструкторе**, как это уже делают `fold.New` и `replay.Run` со своими нулевыми значениями. Проверок на `nil` в местах вызова быть не должно. - [x] 5.4 `internal/httpapi` собирается без `fold`; транспорт по-прежнему не логирует исход и переводит только ошибки приёма. ## 6. Жизненный цикл в `serve.go` - [x] 6.1 Собрать воркер, запустить `Run` в горутине, передать его `Notify` в `ingest`, разбудить при старте — этим и делается подбор `pending`. - [x] 6.2 Остановка: `srv.Shutdown` → отмена контекста воркера → ожидание `done` в остатке того же бюджета `shutdownTimeout` (30 с, как `stop_grace_period`). - [x] 6.3 Контекстная ошибка `Shutdown` — `WARN`, а не отказ команды: stdlib возвращает `DeadlineExceeded` штатно, а `main` печатает на любой ошибке `fatal startup` и выходит с кодом 1. - [x] 6.4 База не закрывается, пока воркер не вышел: `Close` под живой транзакцией свёртки дал бы `ERROR` по доставке, с которой всё в порядке. Не уложились — оставляем закрытие процессу и называем это `WARN`. ## 7. Бюджет ответа маршрута приёма - [x] 7.1 `handleIngest` перед чтением тела ставит дедлайн записи ответа через `http.NewResponseController(w).SetWriteDeadline` на `read_timeout + write_timeout`. `http.ErrNotSupported` — `DEBUG` и продолжение, а не отказ приёма. - [x] 7.2 Общий `write_timeout` и его умолчание не меняются; комментарий в `config.example.toml` объясняет, что он покрывает и чтение тела и потому приём держит собственный бюджет. ## 8. Проверки Приёмочные критерии — рубрика ревью дизайна, перенесена сюда целиком. - [x] 8.1 **Завершимость прохода.** Доставка, оставшаяся `pending`, не выбирается проходом повторно; `Pass` возвращает управление. - [x] 8.2 **Атомарность единицы работы.** Прерванная свёртка оставляет доставку `pending` и не оставляет частично записанных объектов. - [x] 8.3 **Идемпотентность повтора.** Двойная свёртка той же доставки даёт тот же отпечаток витрины. - [x] 8.4 **Сигнал не теряет работу.** Доставка, чей сигнал потерян, всё равно подбирается — тиком или следующим проходом. - [x] 8.5 **Тотальный порядок.** Доставки с одинаковым `received_at` сворачиваются в порядке `id` при любом размере порции; порядок проверяется наблюдаемым следствием — наследованием слоя. - [x] 8.6 **Остановка.** Приём прекращается раньше воркера; после остановки нет доставки, числящейся разобранной и записанной наполовину; исчерпание бюджета не даёт ненулевого кода возврата. - [x] 8.7 **Транзиентный отказ ≠ отказ доставки.** Занятость базы оставляет `pending`, содержимое даёт `failed` (задача 2.2). - [x] 8.8 **Отставание наблюдаемо и при отсутствии прогресса.** Метка не срабатывает на задолженности первого прохода и срабатывает после него; считается на выборке, а не по факту свёртки. - [x] 8.9 **Одна классификация на оба входа.** Табличный тест `Player` (задача 3.4) плюс зелёные существующие тесты `replay`. - [x] 8.10 **Тестируемость без сна.** Ни один тест воркера не ждёт по часам: проверки идут через `Pass`, тест на `Run` — один, «отмена завершает цикл». - [x] 8.11 **Приём не платит за воркер.** После `Accept` доставка числится `pending`, обработчик не ждёт; приём с неработающим воркером отвечает `200`. - [x] 8.12 **Данные о здоровье не в логе.** Записи воркера несут только идентификатор доставки, счётчики и длительности. - [x] 8.13 `task gate` зелёный; `task verify:archive` даёт то же состояние. ## 9. Документация - [x] 9.1 `docs/architecture.md`, раздел «Приём»: ответ отдаётся после архивации и учёта — это **контракт**, а не деталь реализации; очередь — таблица, а не память; порядок журнала и остановка; классификация «отказ доставки» против «отказ обстоятельств»; бюджет ответа маршрута приёма; отвергнутые варианты с причинами. - [x] 9.2 Строка в задачу беклога `stats-endpoint`: длина `pending`, возраст самой старой неразобранной доставки и то, что `/healthz` их не отражает. ## 10. Правки по ревью кода (профиль `deep`) - [x] 10.1 `store.CreateDelivery` — через `inTx` с повторами: одиночная вставка пересиживала только `busy_timeout`, и приём отвечал `500` по доставке, тело которой уже на диске (измерено: окно занятости 6.55 с при широкой свёртке против пяти секунд ожидания). - [x] 10.2 Паника свёртки перехватывается в `fold.Fold` — у той же границы, что пишет исход разбора: в фоновой горутине она валила процесс, а `restart: unless-stopped` превращал дефект одной доставки в цикл перезапуска. Писатель `parse_status` остался единственным. - [x] 10.3 Флаг «первый проход завершён» снимается только у прохода, дошедшего до пустой выборки: взведённый на отказе базы, он включал метку задержки после прохода, который ничего не свернул. - [x] 10.4 Метка отставания — одна запись на проход (число задержанных и худшее ожидание), а не запись на доставку: задолженность в сотню тел давала бы сотню одинаковых `WARN` каждую минуту. - [x] 10.5 Отмена не пишется `ERROR`-ом в цикле воркера; ветка отказа `Serve` в `serve.go` останавливает воркер прежде, чем закрыть базу. - [x] 10.6 Значения заголовков обрезаются перед логом: `automation-name` длиной 600 КБ выдавливал из ротации всю недавнюю историю (измерено). - [x] 10.7 Запасной `store.Now()` для метки приёма убран: он заводил второй источник времени вопреки соседнему комментарию и был недостижим. - [x] 10.8 `classify` неэкспортируема: вторая публичная дверь возвращала `Outcome` без `Partial`, то есть молча занижала счётчик, по которому решается судьба тела. - [x] 10.9 Оракул правила «занятость — обстоятельство»: `task verify:busy` — свёртка под удерживаемой блокировкой. В гейт не входит (25 секунд), но мутацию «убрать ветку `Transient`» убивает. - [x] 10.10 Дельты `storage` и `reindex`: «отказ ⇒ `failed`» сужено до отказов разбора, добавлен класс «отложено». - [x] 10.11 Остаточный предел порядка при конкурентных приёмах назван в спеке и в `docs/architecture.md`, вынут блокером (`docs/tasks/items/journal-order-on-ingest.md`).