chore(openspec): архив change slice-0-skeleton
Срез 0 реализован и влит (PR #3), change перенесён в openspec/changes/archive, спеки — в openspec/specs. Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_012vJkmBGKKqMoEMzvsXG3y2
This commit is contained in:
parent
8ee998d189
commit
49d9eca082
@ -29,17 +29,17 @@
|
|||||||
|
|
||||||
## 3. Ядро (детали — `arm-core/openspec/changes/slice-0-skeleton/tasks.md`)
|
## 3. Ядро (детали — `arm-core/openspec/changes/slice-0-skeleton/tasks.md`)
|
||||||
|
|
||||||
- [ ] 3.1 [arm-core] Инструменты: `api/pyproject.toml` (ruff, mypy), `requirements-dev.txt` с `-r requirements.txt`,
|
- [x] 3.1 [arm-core] Инструменты: `api/pyproject.toml` (ruff, mypy), `requirements-dev.txt` с `-r requirements.txt`,
|
||||||
`Dockerfile` (`python:3.11-slim`, + строка убрана из `.gitignore`), `Makefile` (общие цели + `migrate`, `seed`),
|
`Dockerfile` (`python:3.11-slim`, + строка убрана из `.gitignore`), `Makefile` (общие цели + `migrate`, `seed`),
|
||||||
`ENV_FILE` в Settings, `raisa-*` в `web/package.json` → `optionalDependencies`
|
`ENV_FILE` в Settings, `raisa-*` в `web/package.json` → `optionalDependencies`
|
||||||
- [ ] 3.2 [arm-core] ORM 1:1 с DBML v5.13 + миграция 0001 + `check_dbml_vs_orm.py`; проверка: upgrade → downgrade → upgrade
|
- [x] 3.2 [arm-core] ORM 1:1 с DBML v5.13 + миграция 0001 + `check_dbml_vs_orm.py`; проверка: upgrade → downgrade → upgrade
|
||||||
на compose-PostgreSQL 16, скрипт сверки — 0 расхождений
|
на compose-PostgreSQL 16, скрипт сверки — 0 расхождений
|
||||||
- [ ] 3.3 [arm-core] Seed справочников/ролей/прав + dev-пользователи; auth (JWT, dev-эмитент, `require(permission)`), `GET /api/v1/me`
|
- [x] 3.3 [arm-core] Seed справочников/ролей/прав + dev-пользователи; auth (JWT, dev-эмитент, `require(permission)`), `GET /api/v1/me`
|
||||||
- [ ] 3.4 [arm-core] Вендоренный контракт + outbox-паблишер + подписка `recognition.result` (queue group `arm-core`): ack
|
- [x] 3.4 [arm-core] Вендоренный контракт + outbox-паблишер + подписка `recognition.result` (queue group `arm-core`): ack
|
||||||
`processing`, повторная публикация без ack, дедуп по `result_msg_id`, `dlq`, вахтёр `reply_deadline`; интеграционный
|
`processing`, повторная публикация без ack, дедуп по `result_msg_id`, `dlq`, вахтёр `reply_deadline`; интеграционный
|
||||||
тест на compose: queued → published → processing → done; дубликат результата игнорируется; потерянный запрос
|
тест на compose: queued → published → processing → done; дубликат результата игнорируется; потерянный запрос
|
||||||
публикуется повторно
|
публикуется повторно
|
||||||
- [ ] 3.5 [arm-core] `make check-full` зелёный; итог — в описание PR
|
- [x] 3.5 [arm-core] `make check-full` зелёный; итог — в описание PR
|
||||||
|
|
||||||
## 4. Проверки качества (параллельно с 3.x, после первых тестов)
|
## 4. Проверки качества (параллельно с 3.x, после первых тестов)
|
||||||
|
|
||||||
@ -51,9 +51,9 @@
|
|||||||
|
|
||||||
## 5. Сервисы распознавания (детали — в их change'ах) и профиль `services`
|
## 5. Сервисы распознавания (детали — в их change'ах) и профиль `services`
|
||||||
|
|
||||||
- [ ] 5.1 [tabular-parser] Вырезать фронт/БД, вендоренный контракт, подписчик queue group `tabular-parser` с ack
|
- [x] 5.1 [tabular-parser] Вырезать фронт/БД, вендоренный контракт, подписчик queue group `tabular-parser` с ack
|
||||||
`processing` и заглушкой `NOT_IMPLEMENTED`, Dockerfile 3.11, `make check-full`
|
`processing` и заглушкой `NOT_IMPLEMENTED`, Dockerfile 3.11, `make check-full`
|
||||||
- [ ] 5.2 [scan-gateway] Вырезать фронт/БД, вендоренный контракт, горячие папки (local/smb), эмулятор, подписчик queue group
|
- [x] 5.2 [scan-gateway] Вырезать фронт/БД, вендоренный контракт, горячие папки (local/smb), эмулятор, подписчик queue group
|
||||||
`scan-gateway` с ack и таймаутом, Dockerfile 3.11, `make check-full`
|
`scan-gateway` с ack и таймаутом, Dockerfile 3.11, `make check-full`
|
||||||
- [x] 5.3 [docs] `docker-compose.yml`: профиль `services` — `arm-core` (8000), `tabular-parser` (8001), `scan-gateway`
|
- [x] 5.3 [docs] `docker-compose.yml`: профиль `services` — `arm-core` (8000), `tabular-parser` (8001), `scan-gateway`
|
||||||
(8002), `scan-emulator` (том `hotfolder`, `../../fixtures/ocr:ro`); проверка: `--profile services up --build`,
|
(8002), `scan-emulator` (том `hotfolder`, `../../fixtures/ocr:ro`); проверка: `--profile services up --build`,
|
||||||
57
openspec/specs/dev-environment/spec.md
Normal file
57
openspec/specs/dev-environment/spec.md
Normal file
@ -0,0 +1,57 @@
|
|||||||
|
# dev-environment Specification
|
||||||
|
|
||||||
|
## Purpose
|
||||||
|
Локальное окружение разработки всего workspace одной командой: инфраструктура (PostgreSQL 16, NATS без JetStream, MinIO),
|
||||||
|
эмулятор внешней ИС Сканирование и три сервиса системы, собранные из соседних каталогов workspace.
|
||||||
|
|
||||||
|
## Requirements
|
||||||
|
|
||||||
|
### Requirement: Инфраструктура поднимается одной командой
|
||||||
|
`docker compose -f docs/dev/docker-compose.yml up -d` без профилей SHALL поднять PostgreSQL 16 (БД `armka`,
|
||||||
|
пользователь `armka`, порт 5432), NATS 2.10 **без JetStream** (как на платформе; порты 4222, мониторинг 8222) и MinIO
|
||||||
|
(API 9000, консоль 9001) с уже созданным бакетом `arm-ka`. Все учётные данные — в `docs/dev/.env.example`, копируемом
|
||||||
|
в `docs/dev/.env`.
|
||||||
|
|
||||||
|
#### Scenario: Чистый старт
|
||||||
|
- **WHEN** разработчик выполняет `cp .env.example .env && docker compose up -d` в `docs/dev/` на машине без томов
|
||||||
|
- **THEN** в течение 60 с все три контейнера проходят healthcheck, `psql` подключается к `armka`, `curl :8222/varz`
|
||||||
|
отвечает и `jetstream` в нём отсутствует/выключен, `mc ls local/arm-ka` возвращает пустой бакет
|
||||||
|
|
||||||
|
### Requirement: Профиль сервисов собирает приложения из workspace
|
||||||
|
Профиль `services` SHALL добавить контейнеры `arm-core`, `tabular-parser`, `scan-gateway` и `scan-emulator`, собранные
|
||||||
|
из `../../arm-core`, `../../tabular-parser`, `../../scan-gateway` относительно `docs/dev/` (работает и из зеркального
|
||||||
|
workspace `.wt/<срез>/`). Сервисы читают настройки через переменные `OPENBAO__*` шаблона, `DEBUG=true`, и получают
|
||||||
|
`fixtures/ocr` смонтированным только для чтения в эмулятор.
|
||||||
|
|
||||||
|
#### Scenario: Сервисы стартуют против инфраструктуры compose
|
||||||
|
- **WHEN** выполняется `docker compose --profile services up -d --build`
|
||||||
|
- **THEN** `GET http://localhost:8000/healthcheck` (arm-core), `:8001/healthcheck` (tabular-parser),
|
||||||
|
`:8002/healthcheck` (scan-gateway) отвечают 200 с `version` и `nats = ok`; в логах нет ошибок подключения к NATS/S3
|
||||||
|
|
||||||
|
#### Scenario: Запуск из venv против compose без Nexus
|
||||||
|
- **WHEN** разработчик поднял только инфраструктуру, создал venv командой `make venv` (ставит `requirements.txt` без
|
||||||
|
строк `raisa_*`) и запускает `python run_server.py` из `api/` любого сервиса с `.env`, скопированным из
|
||||||
|
`docs/dev/.env.example`
|
||||||
|
- **THEN** сервис подключается к тем же PostgreSQL/NATS/MinIO по `localhost` и проходит healthcheck
|
||||||
|
|
||||||
|
### Requirement: Эмулятор ИС Сканирование воспроизводит горячие папки
|
||||||
|
Контейнер `scan-emulator` SHALL следить за каталогом `Input/` общего тома `hotfolder`; для файла `UUID_имя.ext` он
|
||||||
|
SHALL через настраиваемую задержку (по умолчанию 2 с) создать `Output/UUID/UUID.json` с содержимым
|
||||||
|
`fixtures/ocr/<имя без расширения>.json`, если такая пара есть; иначе — JSON ошибки в формате Content AI
|
||||||
|
(`error.code = INVALID_DOCUMENT`).
|
||||||
|
|
||||||
|
#### Scenario: Известная пара
|
||||||
|
- **WHEN** в `Input/` появляется `3f1c…_aep_5.pdf`
|
||||||
|
- **THEN** через ≤ 5 с в `Output/3f1c…/3f1c….json` лежит содержимое `fixtures/ocr/aep_5.json`
|
||||||
|
|
||||||
|
#### Scenario: Неизвестное имя
|
||||||
|
- **WHEN** в `Input/` появляется `…_unknown.pdf`
|
||||||
|
- **THEN** в `Output/…/….json` лежит ответ с `error.code = INVALID_DOCUMENT` и пустым `data_ocr`
|
||||||
|
|
||||||
|
### Requirement: Окружение документировано
|
||||||
|
`docs/dev/README.md` SHALL описывать: предварительные требования, команды запуска инфраструктуры и профиля сервисов,
|
||||||
|
переменные `.env`, как запустить интеграционные тесты каждого сервиса против compose, как сбросить тома.
|
||||||
|
|
||||||
|
#### Scenario: Новый разработчик
|
||||||
|
- **WHEN** разработчик следует README без помощи
|
||||||
|
- **THEN** он получает работающее окружение и зелёные интеграционные тесты `arm-core` за одну сессию
|
||||||
56
openspec/specs/quality-checks/spec.md
Normal file
56
openspec/specs/quality-checks/spec.md
Normal file
@ -0,0 +1,56 @@
|
|||||||
|
# quality-checks Specification
|
||||||
|
|
||||||
|
## Purpose
|
||||||
|
Единый набор проверок качества для сервисных репозиториев АРМ КА, выполняемый локально одной командой перед каждым PR:
|
||||||
|
какие проверки обязательны, в каком порядке и с какими внешними ресурсами. Конфигураций CI в репозиториях нет — в Gitea
|
||||||
|
CI не используется, пайплайн платформы банка вне зоны проекта.
|
||||||
|
|
||||||
|
## Requirements
|
||||||
|
|
||||||
|
### Requirement: `make check` — быстрые проверки без Docker
|
||||||
|
Каждый сервисный репозиторий SHALL предоставлять цель `make check`, выполняющую из `api/`: `ruff check .`,
|
||||||
|
`ruff format --check .`, `mypy src`, `pytest -m "not integration"`, тест идентичности вендоренного контракта с
|
||||||
|
`../docs/contracts`. Цель SHALL завершаться ненулевым кодом при первой упавшей проверке и MUST NOT требовать запущенного
|
||||||
|
Docker, PostgreSQL или NATS.
|
||||||
|
|
||||||
|
#### Scenario: Файл не отформатирован
|
||||||
|
- **WHEN** в репозитории есть файл, не отформатированный `ruff format`
|
||||||
|
- **THEN** `make check` завершается с кодом 1 на шаге `ruff format --check`, следующие шаги не выполняются
|
||||||
|
|
||||||
|
#### Scenario: Чистый репозиторий
|
||||||
|
- **WHEN** все проверки проходят
|
||||||
|
- **THEN** `make check` завершается с кодом 0 за время, не превышающее нескольких минут, без обращения к сети
|
||||||
|
|
||||||
|
### Requirement: `make check-full` — полная проверка против compose
|
||||||
|
Каждый сервисный репозиторий SHALL предоставлять цель `make check-full`: `make check` плюс `pytest -m integration` против
|
||||||
|
инфраструктуры `docs/dev/docker-compose.yml` (PostgreSQL 16, NATS, MinIO, для `scan-gateway` — эмулятор). Для `arm-core`
|
||||||
|
цель SHALL дополнительно выполнять «`alembic upgrade head` → `alembic downgrade base` → `alembic upgrade head`» на чистой
|
||||||
|
БД и `python scripts/check_dbml_vs_orm.py` против `../docs/model/data-model-v5.13.dbml`. `make check-full` SHALL быть
|
||||||
|
выполнен перед созданием PR, его итог вписывается в описание PR.
|
||||||
|
|
||||||
|
#### Scenario: Расхождение ORM и DBML
|
||||||
|
- **WHEN** в ORM добавлена колонка, которой нет в DBML
|
||||||
|
- **THEN** шаг `check_dbml_vs_orm` падает с перечнем расхождений, `make check-full` завершается с кодом 1
|
||||||
|
|
||||||
|
#### Scenario: Миграция без отката
|
||||||
|
- **WHEN** `downgrade` ревизии не удаляет созданный ею объект
|
||||||
|
- **THEN** шаг «upgrade → downgrade → upgrade» падает
|
||||||
|
|
||||||
|
### Requirement: Фикстуры и соседние репозитории — опциональны с предупреждением
|
||||||
|
Проверки SHALL брать `docs` и `fixtures` из соседних каталогов workspace (`../docs`, `../fixtures`) или из переменных
|
||||||
|
`ARM_KA_DOCS`, `ARM_KA_FIXTURES`. При отсутствии фикстур тесты с маркером `fixtures` SHALL пропускаться с явной причиной;
|
||||||
|
при отсутствии docs тест идентичности контракта SHALL пропускаться с предупреждением. Отсутствие соседей MUST NOT делать
|
||||||
|
`make check` красным.
|
||||||
|
|
||||||
|
#### Scenario: Нет фикстур
|
||||||
|
- **WHEN** `../fixtures` отсутствует и `ARM_KA_FIXTURES` не задан
|
||||||
|
- **THEN** тесты с маркером `fixtures` помечены skipped с причиной «ARM_KA_FIXTURES недоступен», остальные выполняются
|
||||||
|
|
||||||
|
### Requirement: Одинаковый набор целей во всех репозиториях
|
||||||
|
`Makefile` каждого сервисного репозитория SHALL содержать цели `up`, `test`, `test-integration`, `lint`, `check`,
|
||||||
|
`check-full` с одинаковой семантикой (описана в `docs/dev/checks.md`); репозиторий MAY добавлять свои цели (`migrate`,
|
||||||
|
`seed`, `emulator`), но MUST NOT менять смысл общих.
|
||||||
|
|
||||||
|
#### Scenario: Сверка с описанием
|
||||||
|
- **WHEN** ревьюер сравнивает список общих целей `Makefile` сервиса с `docs/dev/checks.md`
|
||||||
|
- **THEN** все общие цели присутствуют и запускают описанные команды
|
||||||
146
openspec/specs/recognition-contract/spec.md
Normal file
146
openspec/specs/recognition-contract/spec.md
Normal file
@ -0,0 +1,146 @@
|
|||||||
|
# recognition-contract Specification
|
||||||
|
|
||||||
|
## Purpose
|
||||||
|
Контракт обмена между ядром `arm-core` и stateless-сервисами распознавания (`tabular-parser`, `scan-gateway`):
|
||||||
|
тела сообщений NATS `recognition.{tabular|image}.request` и `recognition.result`, файл результата в S3 и правила
|
||||||
|
версионирования и идемпотентности. Сервисы не знают БД; ядро — единственный писатель (DECISION-4).
|
||||||
|
|
||||||
|
## 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`), но из тела извлекаются
|
||||||
|
корректные `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: Подтверждение взятия в работу (ack без JetStream)
|
||||||
|
Получив корректный запрос (прошедший проверку схемы и `MAJOR`), сервис 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 `processing`**: ответом на
|
||||||
|
запрос служит сам финальный конверт, ядро закрывает им задание (`published → error`), и повторной доставки запроса не
|
||||||
|
будет. Конверт публикуется только если из тела извлекаются корректные `job_id` и `file_id`; иначе — как для нечитаемого
|
||||||
|
запроса (ответа нет, ядро закроет задание по `NO_ACK` / `dlq`). `attempt` для `Nats-Msg-Id` берётся из тела запроса; при его
|
||||||
|
отсутствии реализации расходятся (`tabular-parser` берёт `attempt` из заголовка `Nats-Msg-Id` запроса или не отвечает,
|
||||||
|
`scan-gateway` подставляет 1) — см. OQ-71
|
||||||
|
|
||||||
|
### 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 публиковать 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: Копия контракта в сервисе идентична источнику
|
||||||
|
Каждый сервис, читающий или пишущий контракт, SHALL хранить копию файлов схем в `api/contracts/` и проверять в `make check`
|
||||||
|
их байтовую идентичность файлам `docs/contracts/` (`../docs` или путь `ARM_KA_DOCS`; при недоступности — пропуск с предупреждением).
|
||||||
|
|
||||||
|
#### Scenario: Копия устарела
|
||||||
|
- **WHEN** в `docs/contracts/` изменена схема, а копия в сервисе — нет
|
||||||
|
- **THEN** тест идентичности в `make check` сервиса падает с указанием имени файла
|
||||||
Loading…
x
Reference in New Issue
Block a user