# Spec Delta: recognition-job-exchange ## MODIFIED Requirements ### Requirement: Ack сервиса переводит задание в `processing` Получив в `recognition.result` конверт со `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: Inbox принимает результат идемпотентно Для конверта со `status ∈ {done, needs_check, error}` ядро SHALL валидировать его по `recognition-result-message.schema.json`, найти задание по `job_id` и: если `result_msg_id` задания уже равен `Nats-Msg-Id` сообщения — ничего не менять; иначе зарегистрировать файл результата в `s3_object` (`bucket_role = results`, `created_by = `; только при наличии `result_s3`), обновить задание (`status`, `service_name`, `service_version`, `finished_at`, `metrics`, `error_code`, `error_message`, `result_schema_version`, `result_s3_object_id`, `result_msg_id`) и зафиксировать транзакцию. При `status = error` файл задания (если `current_recognition_job_id` равен заданию) SHALL получить `stored_file.status = ERROR`; при `done`/`needs_check` файл остаётся `PROCESSING` и SHALL быть обработан циклом маппинга (спека `osv-result-mapping`); inbox MUST NOT читать файл результата. Неизвестный `job_id` SHALL быть залогирован. Результат для задания, уже находящегося в финальном статусе (`done`, `needs_check`, `error`, `timeout`, `dlq`), SHALL логироваться и не применяться. #### Scenario: Успешный результат - **WHEN** сервис публикует конверт `status = done` с `result_s3 = {arm-ka, results///result.json}` и `Nats-Msg-Id = result::1` - **THEN** задание получает `status = done`, `result_msg_id = result::1`, `result_s3_object_id` указывает на новую строку `s3_object` с этим ключом, `finished_at` заполнен; файл остаётся `PROCESSING` до маппинга #### Scenario: Повторная доставка - **WHEN** то же сообщение получено второй раз - **THEN** строка `recognition_job` и `s3_object` не меняются #### Scenario: Ошибка сервиса - **WHEN** конверт содержит `status = error`, `error = {UNSUPPORTED_FORMAT, …}` без `result_s3` - **THEN** задание получает `status = error`, `error_code = UNSUPPORTED_FORMAT`, `error_message` из конверта, `result_s3_object_id` пуст; файл — `ERROR` #### Scenario: Результат пришёл после таймаута - **WHEN** задание уже `timeout`, и приходит конверт `done` - **THEN** задание не меняется, событие залогировано ### Requirement: Таймаут ожидания результата Фоновый вахтёр SHALL с периодом 30 с переводить задания `processing` и `published` с `reply_deadline < now()` в `status = timeout`, `error_code = TIMEOUT`, `finished_at = now()`, а их файлы (по `current_recognition_job_id`) — в `stored_file.status = ERROR`. Публикатор при переводе задания в `dlq` SHALL так же переводить файл в `ERROR`. Replay задания (новая попытка, `attempt + 1`) — вне этого среза. #### Scenario: Сервис взял, но не ответил - **WHEN** задание в `processing` и `reply_deadline` прошёл без результата - **THEN** не позднее чем через 30 с задание имеет `status = timeout`, файл — `ERROR` с `error_code = TIMEOUT`