Andrey Epifancev da9b9f66cf openspec: change nats-env-config — subject'ы и JWT NATS по окружению из OpenBao
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-13 13:38:50 +04:00

97 lines
9.5 KiB
Markdown
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

## ADDED Requirements
### Requirement: Subject'ы NATS — настройки окружения
Subject'ы обмена SHALL задаваться секретами группы `OPENBAO__NATS__` с одинаковыми ключами во всех трёх сервисах:
`NATS_TABULAR_REQUEST_SUBJECT` (по умолчанию `files.tabular_uploaded`), `NATS_IMAGE_REQUEST_SUBJECT` (по умолчанию
`files.img_uploaded`), `NATS_RESULT_SUBJECT` (по умолчанию `files.tabular_parsed`). У ядра и сервисов одного окружения
значения MUST совпадать. Каждый сервис SHALL при старте проверять все три имени: непустое, без пробельных символов, без
шаблонов `*` и `>`, без пустых токенов, три имени попарно различны; при нарушении сервис MUST NOT стартовать.
Учётные данные NATS (`OPENBAO__NATS__NATS_JWT`, `OPENBAO__NATS__NATS_SEED`) SHALL задаваться по окружению теми же
секретами, и их значения MUST NOT попадать в логи, `repr` настроек и тексты ошибок валидации конфигурации. Каждый
сервис SHALL писать используемые subject'ы в лог при старте и отдавать их в `/healthcheck` объектом `subjects`
(ключи `tabular_request`, `image_request`, `result` — только те, что использует сервис) независимо от состояния NATS.
Queue group'ы и `schema_version` от настроек subject'ов не зависят.
#### Scenario: Умолчания
- **WHEN** сервис стартует без ключей `OPENBAO__NATS__NATS_*_SUBJECT`
- **THEN** обмен идёт по `files.tabular_uploaded`, `files.img_uploaded`, `files.tabular_parsed`
#### Scenario: Набор тестового окружения
- **WHEN** всем трём сервисам заданы одинаковые `OPENBAO__NATS__NATS_TABULAR_REQUEST_SUBJECT`,
`NATS_IMAGE_REQUEST_SUBJECT`, `NATS_RESULT_SUBJECT`, `NATS_JWT`, `NATS_SEED` тестового окружения
- **THEN** ядро публикует запросы и получает ack и результаты только по этим subject'ам; subject'ы по умолчанию не
используются, `recognition_job.request_subject` содержит subject тестового окружения
#### Scenario: Недопустимый набор
- **WHEN** `NATS_RESULT_SUBJECT` совпадает с одним из subject'ов запроса, или имя содержит `*`, `>` или пробел
- **THEN** сервис не стартует с ошибкой конфигурации, называющей ключ
#### Scenario: Разъехавшиеся настройки видны
- **WHEN** у ядра и сервиса заданы разные subject'ы ответа
- **THEN** `/healthcheck.subjects.result` ядра и сервиса различаются; JWT и seed в ответе `/healthcheck` отсутствуют
#### Scenario: Ошибка конфигурации не раскрывает JWT
- **WHEN** сервис не стартует из-за ошибки валидации настроек при заданных `NATS_JWT` и `NATS_SEED`
- **THEN** текст ошибки и лог не содержат значений JWT и seed
## MODIFIED Requirements
### Requirement: Сообщение запроса распознавания
Тело сообщения на subject запроса `tabular` или `image` (настройки окружения `NATS_TABULAR_REQUEST_SUBJECT` /
`NATS_IMAGE_REQUEST_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` = `<job_id>:<attempt>`
#### 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:<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: Сообщение результата распознавания
Тело сообщения на subject ответа (настройка окружения `NATS_RESULT_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:<job_id>:<attempt>`
#### 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` может отсутствовать