## MODIFIED Requirements ### Requirement: Outbox публикует задания из `queued` Фоновый публикатор SHALL с периодом `OUTBOX_POLL_SECONDS` (по умолчанию 5) выбирать задания `status = queued` (с блокировкой `FOR UPDATE SKIP LOCKED`), формировать сообщение по `recognition-request.schema.json` v1.0 из `recognition_job ⋈ stored_file ⋈ s3_object` и публиковать в subject по `pipeline` с заголовком `Nats-Msg-Id = :`. После успешной публикации задание SHALL получить `status = published`, `published_at`, `request_msg_id`, `request_subject`; при ошибке публикации задание SHALL остаться `queued`. #### Scenario: Публикация запроса парсеру - **WHEN** в `recognition_job` вставлена строка `queued` с `pipeline = tabular`, связанная со `stored_file` (XLSX) и его `s3_object` - **THEN** в течение 10 с в `files.tabular_uploaded` появляется сообщение, проходящее схему запроса, с `job_id`, `file_id`, `s3.key` исходника, `reply_deadline`, `attempt = 1`, а строка переходит в `published` с заполненным `request_msg_id` #### Scenario: NATS недоступен - **WHEN** публикация завершается ошибкой соединения - **THEN** задание остаётся `queued`, ошибка залогирована, следующий цикл повторяет публикацию ### Requirement: Ack сервиса переводит задание в `processing` Получив в `files.tabular_parsed` конверт со `status = processing` для задания в `published`, ядро SHALL установить `status = processing`, `started_at`, `service_name`, `service_version` и перевести файл задания в `stored_file.status = PROCESSING`, если `current_recognition_job_id` файла равен этому заданию. Повторный ack для задания в `processing` SHALL игнорироваться; ack для задания в другом статусе SHALL логироваться и не менять задание. #### Scenario: Сервис подтвердил - **WHEN** сервис опубликовал конверт `processing` с `job_id` опубликованного задания - **THEN** задание имеет `status = processing`, `started_at` заполнен, `service_name = tabular-parser`, файл — `PROCESSING` ### Requirement: Соединение с NATS по шаблону, без JetStream Ядро SHALL подключаться к NATS с nkeys-seed и JWT, если заданы `OPENBAO__NATS__NATS_SEED/NATS_JWT`, и без учётных данных, если они пусты; подписываться на `files.tabular_parsed` в queue group `arm-core`; переподключаться автоматически. Stream'ы и consumer'ы JetStream MUST NOT требоваться. Фоновые задачи SHALL запускаться при `NATS_ENABLED=true`. #### Scenario: Dev-окружение compose - **WHEN** ядро стартует с `DEBUG=true`, `NATS_ENABLED=true` и пустыми учётными данными против NATS без JetStream - **THEN** подписка активна, `/healthcheck` отдаёт `nats = ok`, в логах нет ошибок подключения #### Scenario: Два инстанса ядра - **WHEN** запущены два инстанса с `NATS_ENABLED=true` и опубликован один результат - **THEN** результат обработан ровно одним инстансом (queue group), строка задания изменена один раз #### Scenario: NATS выключен - **WHEN** `NATS_ENABLED=false` - **THEN** приложение стартует, фоновые задачи не запускаются, `/healthcheck` отвечает 200 с `nats = off`