## MODIFIED Requirements ### Requirement: Сообщение запроса распознавания Тело сообщения на subject `files.tabular_uploaded` или `files.img_uploaded` SHALL соответствовать `docs/contracts/recognition-request.schema.json` v1.0: обязательные поля `schema_version` (`"1.0"`), `job_id` (uuid = `recognition_job.id`), `file_id` (uuid = `stored_file.id`), `pipeline` (`tabular` | `image`), `file_type` (`XLS` | `XLSX` | `XLSB` | `PDF` | `PNG` | `JPG`), `original_name`, `s3 {bucket, key}` исходника в роли `uploads`, `reply_deadline` (date-time, UTC), `attempt` (≥ 1). Subject SHALL соответствовать `pipeline`. #### Scenario: Публикация запроса ядром - **WHEN** ядро переводит `recognition_job` из `queued` в `published` - **THEN** в subject, соответствующий `recognition_job.pipeline`, публикуется сообщение, проходящее схему запроса, с заголовком `Nats-Msg-Id` = `:` #### Scenario: Сообщение не по схеме - **WHEN** сервис получает запрос, не проходящий схему (нет `s3.key`, неизвестный `file_type`), но из тела извлекаются корректные `job_id` и `file_id` - **THEN** сервис публикует результат со `status = error`, `error.code = SCHEMA_MISMATCH` **без предшествующего ack `processing`** и не падает; `attempt` для `Nats-Msg-Id` результата берётся из тела запроса, при его отсутствии реализации расходятся (`tabular-parser` берёт `attempt` из заголовка `Nats-Msg-Id` запроса или не отвечает, `scan-gateway` подставляет 1) — см. OQ-71 #### Scenario: Нечитаемый запрос - **WHEN** сервис получает сообщение, из которого нельзя извлечь `job_id` (не JSON, не объект, `job_id` отсутствует или не uuid), а значит нельзя собрать валидный конверт - **THEN** сервис ответа не публикует: запрос записывается в лог и отбрасывается; ядро не получит ack, повторит публикацию `OUTBOX_MAX_REPUBLISH` раз и переведёт задание в `dlq` (`error_code = NO_ACK`) #### Scenario: Невалидный повтор запроса, уже находящегося в работе - **WHEN** сервис получает запрос, не проходящий схему, но с `job_id`, который уже в работе или обработан этим процессом - **THEN** конверт `SCHEMA_MISMATCH` не публикуется (его `Nats-Msg-Id` `result::` совпал бы с настоящим результатом, и ядро отбросило бы настоящий как дубликат); запрос записывается в лог и отбрасывается #### Scenario: Примеры контракта — иллюстрации - **WHEN** запрос берётся из `docs/contracts/examples/request-*.json` (в `request-image.json` `reply_deadline` лежит в прошлом) - **THEN** он проходит схему: схема проверяет для `reply_deadline` лишь формат `date-time`, но не сравнивает срок с текущим временем; пример — иллюстрация формы сообщения, а не готовое к исполнению задание; проверка срока — дело сервиса (`scan-gateway` отвечает `error TIMEOUT`, если `reply_deadline` истёк до отправки файла в ИС Сканирование) ### Requirement: Подтверждение взятия в работу (ack без JetStream) Получив корректный запрос (прошедший проверку схемы и `MAJOR`), сервис SHALL немедленно, до чтения файла из S3, опубликовать в `files.tabular_parsed` конверт со `status = processing`, `job_id`, `file_id`, `service {name, version}` и заголовком `Nats-Msg-Id = ack::`. Ядро SHALL перевести задание `published → processing` и записать `started_at`, `service_name`, `service_version`. Если ack не получен за `ACK_TIMEOUT_SECONDS` (по умолчанию 30), ядро SHALL опубликовать запрос повторно с тем же `Nats-Msg-Id` и тем же `attempt`, не более `OUTBOX_MAX_REPUBLISH` (по умолчанию 3) раз, после чего перевести задание в `dlq`. #### Scenario: Сервис жив - **WHEN** запрос опубликован и сервис подписан - **THEN** в течение `ACK_TIMEOUT_SECONDS` в `files.tabular_parsed` появляется конверт `processing`, задание получает `status = processing` и `started_at` #### Scenario: Сервис недоступен - **WHEN** ни один инстанс сервиса не подписан на subject запроса - **THEN** ядро публикует запрос повторно каждые `ACK_TIMEOUT_SECONDS`; после `OUTBOX_MAX_REPUBLISH` повторов задание получает `status = dlq`, `request_msg_id` не меняется #### Scenario: Повторный запрос пришёл в работающий сервис - **WHEN** сервис получает запрос с `job_id`, который уже обрабатывается или обработан этим процессом - **THEN** сервис публикует ack повторно и не начинает вторую обработку ### Requirement: Сообщение результата распознавания Тело сообщения на subject `files.tabular_parsed` SHALL соответствовать `docs/contracts/recognition-result-message.schema.json` v1.0: обязательные поля `schema_version` (`"1.0"`), `job_id`, `file_id`, `status` (`processing` | `done` | `needs_check` | `error`), `service {name, version}`; при `status ∈ {done, needs_check}` обязательны `result_s3 {bucket, key}` и `result_schema_version`; при `status = error` обязателен `error {code, message}`; опциональны `raw_s3_key` (только `scan-gateway`) и `metrics`. Поле `group_schema` в конверт NOT включается — источник `recognition_job.group_schema` — файл результата. #### Scenario: Успешный результат - **WHEN** сервис завершил обработку файла - **THEN** файл результата записан в S3 по ключу `results/{file_id}/{job_id}/result.json`, а затем опубликован конверт со `status = done | needs_check`, `result_s3` указывает на этот файл, заголовок `Nats-Msg-Id` = `result::` #### Scenario: Ошибка обработки - **WHEN** сервис не смог обработать файл (нечитаем, таймаут внешней системы, тип не поддерживается) - **THEN** публикуется конверт со `status = error` и `error.code` из перечня схемы (`UNSUPPORTED_FORMAT`, `UNREADABLE`, `TIMEOUT`, `SCHEMA_MISMATCH`, `NOT_IMPLEMENTED`, `FILE_READ_ERROR`, `INVALID_FORMAT`, `INVALID_DOCUMENT`, `RECOGNITION_FAILED`, `UNKNOWN_ERROR`); `result_s3` может отсутствовать