docs(openspec): recognition-contract — SCHEMA_MISMATCH без ack, нечитаемый запрос, повторы публикации, примеры как иллюстрации

Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
This commit is contained in:
Andrey Epifancev 2026-10-04 06:41:29 +04:00
parent a977ab7792
commit 2423854f27

View File

@ -21,8 +21,28 @@
заголовком `Nats-Msg-Id` = `<job_id>:<attempt>` заголовком `Nats-Msg-Id` = `<job_id>:<attempt>`
#### Scenario: Сообщение не по схеме #### Scenario: Сообщение не по схеме
- **WHEN** сервис получает запрос, не проходящий схему (нет `s3.key`, неизвестный `file_type`) - **WHEN** сервис получает запрос, не проходящий схему (нет `s3.key`, неизвестный `file_type`), но из тела извлекаются
- **THEN** сервис публикует результат со `status = error`, `error.code = SCHEMA_MISMATCH` и не падает корректные `job_id` и `file_id` (и `attempt` для `Nats-Msg-Id`)
- **THEN** сервис публикует результат со `status = error`, `error.code = SCHEMA_MISMATCH` **без предшествующего ack
`processing`** и не падает
#### 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:<job_id>:<attempt>` совпал бы с настоящим
результатом, и ядро отбросило бы настоящий как дубликат); запрос записывается в лог и отбрасывается
#### 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) ### Requirement: Подтверждение взятия в работу (ack без JetStream)
Получив запрос, сервис SHALL немедленно, до чтения файла из S3, опубликовать в `recognition.result` конверт со Получив запрос, сервис SHALL немедленно, до чтения файла из S3, опубликовать в `recognition.result` конверт со
@ -82,8 +102,10 @@ v1.0: обязательные поля `schema_version` (`"1.0"`), `job_id`, `f
#### Scenario: Несовместимая версия запроса #### Scenario: Несовместимая версия запроса
- **WHEN** сервис, поддерживающий `1.x`, получает запрос `schema_version = "2.0"` - **WHEN** сервис, поддерживающий `1.x`, получает запрос `schema_version = "2.0"`
- **THEN** публикуется результат `status = error`, `error.code = SCHEMA_MISMATCH`, запрос подтверждается (ack), чтобы не - **THEN** публикуется результат `status = error`, `error.code = SCHEMA_MISMATCH` **без ack `processing`**: ответом на
зациклить доставку запрос служит сам финальный конверт, ядро закрывает им задание (`published → error`), и повторной доставки запроса не
будет. Конверт публикуется только если из тела извлекаются корректные `job_id` и `file_id`; иначе — как для нечитаемого
запроса (ответа нет, ядро закроет задание по `NO_ACK` / `dlq`)
### Requirement: Идемпотентность результатов без брокера ### Requirement: Идемпотентность результатов без брокера
Публикующий SHALL ставить заголовок `Nats-Msg-Id` на каждое сообщение (запрос `<job_id>:<attempt>`, ack Публикующий SHALL ставить заголовок `Nats-Msg-Id` на каждое сообщение (запрос `<job_id>:<attempt>`, ack
@ -95,6 +117,23 @@ v1.0: обязательные поля `schema_version` (`"1.0"`), `job_id`, `f
- **WHEN** сервис опубликовал один и тот же результат два раза (повтор публикации после сетевой ошибки) - **WHEN** сервис опубликовал один и тот же результат два раза (повтор публикации после сетевой ошибки)
- **THEN** задание изменено один раз, второе сообщение проигнорировано - **THEN** задание изменено один раз, второе сообщение проигнорировано
### Requirement: Повторы публикации ответов сервисом
Сервис SHALL публиковать ack и результат с заголовком `Nats-Msg-Id` и при временной ошибке публикации (потеря соединения,
таймаут подтверждения сервера) повторять публикацию с тем же `Nats-Msg-Id`: **одна первая попытка и до
`PUBLISH_RETRIES` повторов** (значение по умолчанию 3, то есть не более 4 попыток), с паузой между попытками; пауза —
деталь реализации. Повторы исчерпаны — сервис пишет error-лог и не публикует это сообщение снова; задание закрывается
на стороне ядра (не пришёл результат — `reply_deadline` → `timeout`; не пришёл ack — повтор запроса и `dlq`, но результат,
дошедший без ack, ядро принимает и в статусе `published`). Дубликаты, появившиеся из-за повтора,
безопасны: ядро дедуплицирует их по `Nats-Msg-Id`.
#### Scenario: Временный сбой публикации
- **WHEN** публикация результата упала с временной ошибкой, а следующая попытка успешна
- **THEN** сообщение опубликовано с прежним `Nats-Msg-Id`; ядро изменит задание один раз, даже если первая попытка тоже дошла
#### Scenario: Повторы исчерпаны
- **WHEN** все 1 + `PUBLISH_RETRIES` попыток публикации результата неуспешны
- **THEN** сервис пишет error-лог и продолжает работу (процесс не падает), а ядро закрывает задание по `reply_deadline`
### Requirement: Копия контракта в сервисе идентична источнику ### Requirement: Копия контракта в сервисе идентична источнику
Каждый сервис, читающий или пишущий контракт, SHALL хранить копию файлов схем в `api/contracts/` и проверять в `make check` Каждый сервис, читающий или пишущий контракт, SHALL хранить копию файлов схем в `api/contracts/` и проверять в `make check`
их байтовую идентичность файлам `docs/contracts/` (`../docs` или путь `ARM_KA_DOCS`; при недоступности — пропуск с предупреждением). их байтовую идентичность файлам `docs/contracts/` (`../docs` или путь `ARM_KA_DOCS`; при недоступности — пропуск с предупреждением).