Andrey Epifancev a0be5ab5a1 docs: Срез 0 под факты платформы — NATS без JetStream, Python 3.11, PostgreSQL 16, без CI и Nexus
По ответам пользователя 03.10.2026: обмен через recognition_job перестроен на core NATS (queue group'ы,
ack сервиса = конверт status=processing, повторная публикация ядром 30 с × 3 → dlq, дедуп по result_msg_id);
capability ci-template заменена на quality-checks (make check / make check-full, конфигураций CI нет);
compose: PostgreSQL 16, NATS без --jetstream, python:3.11-slim; пакеты raisa_* фильтруются при dev-установке,
npm raisa-* → optionalDependencies. Обновлены 03-architecture, 09-delivery-plan, 00-workspace-layout,
proposal/design/specs/tasks change'а; открытые вопросы к платформе закрыты, остались Node и JWT РАИСА.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
2026-10-03 16:21:39 +04:00

105 lines
9.4 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.

# Spec Delta: recognition-contract
## Purpose
Контракт обмена между ядром `arm-core` и stateless-сервисами распознавания (`tabular-parser`, `scan-gateway`):
тела сообщений NATS `recognition.{tabular|image}.request` и `recognition.result`, файл результата в S3 и правила
версионирования и идемпотентности. Сервисы не знают БД; ядро — единственный писатель (DECISION-4).
## ADDED Requirements
### Requirement: Сообщение запроса распознавания
Тело сообщения на subject `recognition.tabular.request` или `recognition.image.request` 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`)
- **THEN** сервис публикует результат со `status = error`, `error.code = SCHEMA_MISMATCH` и не падает
### Requirement: Подтверждение взятия в работу (ack без JetStream)
Получив запрос, сервис SHALL немедленно, до чтения файла из S3, опубликовать в `recognition.result` конверт со
`status = processing`, `job_id`, `file_id`, `service {name, version}` и заголовком `Nats-Msg-Id = ack:<job_id>:<attempt>`.
Ядро 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` в `recognition.result` появляется конверт `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 `recognition.result` 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` может отсутствовать
### Requirement: Файл результата
Файл результата SHALL соответствовать `docs/contracts/recognition-result.schema.json` v1.0: один файл (`stored_file`)
→ один `job_id` → массив `documents` из `osvDocument` и/или `statementDocument`; денежные значения — строки
`^-?\d+(\.\d{1,2})?$`; `confidence` — 0–100. Пустой массив `documents` допустим только при `status = error`.
#### Scenario: Результат валидируется примерами
- **WHEN** CI сервиса запускает тесты контракта
- **THEN** все файлы `docs/contracts/examples/result-*.json` проходят схему файла результата, а все
`request-*.json` / `result-message-*.json` — схемы сообщений
### Requirement: Версионирование контракта
Поле `schema_version` SHALL быть строкой `MAJOR.MINOR`. Добавление необязательных полей или значений enum —
`MINOR` (1.1, обратно совместимо); удаление/переименование полей, смена типов или семантики — `MAJOR` (2.0).
Сервис SHALL отвергать запрос с неизвестным `MAJOR` (`error.code = SCHEMA_MISMATCH`); ядро SHALL принимать результат
с тем же `MAJOR` и любым известным или новым `MINOR`.
#### Scenario: Несовместимая версия запроса
- **WHEN** сервис, поддерживающий `1.x`, получает запрос `schema_version = "2.0"`
- **THEN** публикуется результат `status = error`, `error.code = SCHEMA_MISMATCH`, запрос подтверждается (ack), чтобы не
зациклить доставку
### Requirement: Идемпотентность результатов без брокера
Публикующий SHALL ставить заголовок `Nats-Msg-Id` на каждое сообщение (запрос `<job_id>:<attempt>`, ack
`ack:<job_id>:<attempt>`, результат `result:<job_id>:<attempt>`). Поскольку серверного дедупа нет, ядро SHALL считать
результат дубликатом, если `recognition_job.result_msg_id` уже равен `Nats-Msg-Id` сообщения, и MUST NOT менять задание
повторно; потерянный результат SHALL обнаруживаться по `reply_deadline` (`timeout`).
#### Scenario: Результат доставлен дважды
- **WHEN** сервис опубликовал один и тот же результат два раза (повтор публикации после сетевой ошибки)
- **THEN** задание изменено один раз, второе сообщение проигнорировано
### Requirement: Копия контракта в сервисе идентична источнику
Каждый сервис, читающий или пишущий контракт, SHALL хранить копию файлов схем в `api/contracts/` и проверять в `make check`
их байтовую идентичность файлам `docs/contracts/` (`../docs` или путь `ARM_KA_DOCS`; при недоступности — пропуск с предупреждением).
#### Scenario: Копия устарела
- **WHEN** в `docs/contracts/` изменена схема, а копия в сервисе — нет
- **THEN** тест идентичности в `make check` сервиса падает с указанием имени файла