Merge pull request 'NATS: subject'ы как в старом АРМ — files.*_uploaded, files.tabular_parsed (#74)' (#77) from feat/74-nats-files-subjects into main
Reviewed-on: #77
This commit is contained in:
commit
d44e4fcb0b
@ -12,9 +12,9 @@
|
||||
PostgreSQL │ │ S3 (uploads/ results/ exports/ archive/)
|
||||
│ │
|
||||
NATS (core) ◀─────────────────┘ outbox │
|
||||
recognition.tabular.request │
|
||||
recognition.image.request │
|
||||
recognition.result ───────────────────▶ arm-core (inbox, дедуп по msg_id)
|
||||
files.tabular_uploaded │
|
||||
files.img_uploaded │
|
||||
files.tabular_parsed ─────────────────────▶ arm-core (inbox, дедуп по msg_id)
|
||||
│
|
||||
┌───────────────┴───────────────┐
|
||||
┌────────▼────────┐ ┌─────────▼────────┐ ┌──────────────────┐
|
||||
@ -27,7 +27,7 @@
|
||||
- **arm-core** — монолитное ядро: справочники, файлы/документы, маппинг результатов распознавания, консолидация
|
||||
(вычисления), согласование, ЕХ, аудит, права. Один Alembic, одна схема.
|
||||
- **tabular-parser** — читает файл из S3 `uploads/`, находит на листах ОСВ (§1) и формы ББ/ОФР (§12) **по содержимому**,
|
||||
пишет нормализованный JSON в S3 `results/`, публикует `recognition.result`. Внутри — `BookParser` (оркестратор книги:
|
||||
пишет нормализованный JSON в S3 `results/`, публикует `files.tabular_parsed`. Внутри — `BookParser` (оркестратор книги:
|
||||
лимиты, чтение, дедлайн, вид каждого листа): листы ОСВ идут в конвейер `osv.py`, листы форм — в конвейер форм
|
||||
`statements/` (блоки форм, колонки, шапка, строки, соотношения §7, склейка продолжений); документы результата — сначала
|
||||
ОСВ, затем формы. Контракт результата —
|
||||
@ -43,7 +43,7 @@
|
||||
`Output/<job_id>/` и входящий файл — **только под флагом `SCAN_CLEANUP`, по умолчанию выключен** (права на удаление
|
||||
на SMB-шаре контура не подтверждены, OQ-144), нормализовать в тот же контракт результата (`OcrNormalizer` по
|
||||
`05-domain-rules.md` §11/§11.1: один `statementDocument` на таблицу ответа, справочники `api/rules/` с редакциями
|
||||
форм), опубликовать `recognition.result`. Таймаут 5 мин → `TIMEOUT`. Раскладка папок — как у проверенного в контуре
|
||||
форм), опубликовать `files.tabular_parsed`. Таймаут 5 мин → `TIMEOUT`. Раскладка папок — как у проверенного в контуре
|
||||
PDFWorker старого АРМ (`RSHB/armka_file_worker`, решение владельца 06.10.2026, OQ-137) с двумя отличиями: временный
|
||||
файл лежит в `Tmp`, а не рядом с назначением, очистка под флагом. Эмулятор ИС Сканирование (dev) ищет пару PDF + JSON
|
||||
по sha256 содержимого входящего файла, а не по имени. Реализация — arm-scan-gateway #6 (change
|
||||
@ -65,16 +65,16 @@
|
||||
|
||||
| Поток | Subject | Кто пишет | Полезная нагрузка | `Nats-Msg-Id` | Схема |
|
||||
|---|---|---|---|---|---|
|
||||
| запрос | `recognition.tabular.request` / `recognition.image.request` | arm-core (outbox из `recognition_job`) | `schema_version`, `job_id`, `file_id`, `pipeline`, `file_type`, `original_name`, `s3 {bucket, key}` исходника, `reply_deadline`, `attempt` | `<job_id>:<attempt>` | [request](../contracts/recognition-request.schema.json) |
|
||||
| ack | `recognition.result` | tabular-parser / scan-gateway | конверт со `schema_version`, `status = processing`, `job_id`, `file_id`, `service {name, version}` | `ack:<job_id>:<attempt>` | [message](../contracts/recognition-result-message.schema.json) |
|
||||
| результат | `recognition.result` | tabular-parser / scan-gateway | `schema_version`, `job_id`, `file_id`, `status` (done / needs_check / error), `service {name, version}`, `metrics` (опц.), `raw_s3_key` (опц., только scan-gateway); при `done` / `needs_check` обязательны `result_s3 {bucket, key}` и `result_schema_version`, при `error` — `error {code, message}` | `result:<job_id>:<attempt>` | [message](../contracts/recognition-result-message.schema.json); файл в S3 — [result](../contracts/recognition-result.schema.json) |
|
||||
| запрос | `files.tabular_uploaded` / `files.img_uploaded` | arm-core (outbox из `recognition_job`) | `schema_version`, `job_id`, `file_id`, `pipeline`, `file_type`, `original_name`, `s3 {bucket, key}` исходника, `reply_deadline`, `attempt` | `<job_id>:<attempt>` | [request](../contracts/recognition-request.schema.json) |
|
||||
| ack | `files.tabular_parsed` | tabular-parser / scan-gateway | конверт со `schema_version`, `status = processing`, `job_id`, `file_id`, `service {name, version}` | `ack:<job_id>:<attempt>` | [message](../contracts/recognition-result-message.schema.json) |
|
||||
| результат | `files.tabular_parsed` | tabular-parser / scan-gateway | `schema_version`, `job_id`, `file_id`, `status` (done / needs_check / error), `service {name, version}`, `metrics` (опц.), `raw_s3_key` (опц., только scan-gateway); при `done` / `needs_check` обязательны `result_s3 {bucket, key}` и `result_schema_version`, при `error` — `error {code, message}` | `result:<job_id>:<attempt>` | [message](../contracts/recognition-result-message.schema.json); файл в S3 — [result](../contracts/recognition-result.schema.json) |
|
||||
|
||||
`group_schema` в конверт не входит — ядро берёт его из `documents[].group_schema` файла результата при маппинге.
|
||||
|
||||
**На платформе NATS без JetStream** (ответ 03.10.2026; подтверждено 06.10.2026 кодом `RSHB/armka_file_worker`, который
|
||||
работает в контуре на `subscribe`/`publish` + `flush` без `jetstream()`): нет durable consumer'ов, серверного дедупа и redelivery.
|
||||
Поэтому гарантии доставки — на уровне приложения: сервисы подписаны queue group'ами (`tabular-parser`, `scan-gateway`),
|
||||
ядро — queue group `arm-core` на `recognition.result`; сервис сразу по получении корректного запроса публикует ack `processing`;
|
||||
ядро — queue group `arm-core` на `files.tabular_parsed`; сервис сразу по получении корректного запроса публикует ack `processing`;
|
||||
ядро, не получив ack за `ACK_TIMEOUT_SECONDS` (30), публикует запрос повторно с тем же `Nats-Msg-Id`, не более
|
||||
`OUTBOX_MAX_REPUBLISH` (3) раз, затем `dlq`. Сервис держит process-local множество `job_id` в работе и на повторный
|
||||
запрос отвечает ack'ом без второй обработки. Публикация ack и результата сервисом при временной ошибке повторяется
|
||||
@ -330,9 +330,11 @@ FastAPI» платформы РАИСА/aurora): дерево импортиро
|
||||
- **NATS** — `nats-py`; аутентификация nkeys-seed + JWT (`NATS_SEED`, `NATS_JWT`) через `user_jwt_cb`/`signature_cb`;
|
||||
соединение и подписки создаются в lifespan и живут в `app.state.nats` / `app.state.nats_subs`. Core NATS без
|
||||
JetStream: подписки с queue group, заголовки `Nats-Msg-Id` ставим сами, ack и повторы — на уровне приложения.
|
||||
Учётные данные одни на все сервисы АРМ: один JWT с правами по subject'ам (ответ владельца 06.10.2026), поэтому новые
|
||||
subject'ы (`recognition.*`) должны быть внесены в права этого JWT платформой; старый АРМ и `armka_file_worker`
|
||||
работают на семействе `files.*` (`files.tabular_uploaded`, `files.tabular_parsed`, `files.img_parsed`).
|
||||
Учётные данные одни на все сервисы АРМ: один JWT с правами по subject'ам (ответ владельца 06.10.2026).
|
||||
Subject'ы — **как в старом АРМ** (решение владельца 08.10.2026): на проде заведены `files.tabular_uploaded`,
|
||||
`files.img_uploaded`, `files.tabular_parsed`, `files.img_parsed`; используем три — запросы в `files.tabular_uploaded`
|
||||
и `files.img_uploaded`, ack и результаты обоих сервисов в общий `files.tabular_parsed`; `files.img_parsed` не
|
||||
используется. Старый АРМ гасится, общих подписчиков на этих subject'ах у нас с ним не будет.
|
||||
- **SMB** (горячие папки ИС Сканирование) — `smbprotocol`/`smbclient`, сессия регистрируется в lifespan; нужен только `scan-gateway`.
|
||||
- **Внешняя аутентификация** — пакет `raisa_fastapi_protected_api` из внутреннего Nexus (`AuthorizationMiddleware`,
|
||||
`ProtectedSettings`, …), импортируется только при `DEBUG=false` (в контуре временно `DEBUG=true`, см. «Аутентификация и авторизация», п. 6); фронтовая пара — `raisa-fastapi-protected-web`.
|
||||
@ -409,7 +411,7 @@ FastAPI» платформы РАИСА/aurora): дерево импортиро
|
||||
## Что ожидаем от платформы (для запроса ресурсов)
|
||||
|
||||
По ответам платформы (03.10.2026): PostgreSQL **16** (одна БД, схема `dkb` — создавать схемы можно), NATS **без
|
||||
JetStream** (3 subject `recognition.*`; аутентификация nkeys/JWT, как в шаблоне), S3-совместимое хранилище (один бакет;
|
||||
JetStream** (3 subject `files.*` — как в старом АРМ, решение владельца 08.10.2026; аутентификация nkeys/JWT, как в шаблоне), S3-совместимое хранилище (один бакет;
|
||||
префиксы-роли + lifecycle по префиксам), Python 3.11 в базовом образе, Nexus (pip/npm, пакеты `raisa_*`) — только внутри
|
||||
банка, CI/CD — пайплайн платформы (мы отдаём команды `make check` / `make check-full`). Секреты — через переменные
|
||||
окружения (`OPENBAO__*`). Открыто: версия Node, параметры JWT РАИСА.
|
||||
|
||||
@ -34,7 +34,7 @@
|
||||
для BASIC/EXTENDED (OQ-46).
|
||||
- Auth: валидация JWT + dev-эмитент; `app_user` seed (по одному на роль); зависимость `require(permission)`.
|
||||
- `audit_event` helper; `s3_object` клиент; outbox-паблишер (ack `processing`, повторная публикация без ack, `dlq`) и
|
||||
подписка `recognition.result` (дедуп по `result_msg_id`, пустой маппер документов).
|
||||
подписка `files.tabular_parsed` (дедуп по `result_msg_id`, пустой маппер документов).
|
||||
- Проверки качества вместо CI (в Gitea CI нет, пайплайн платформы вне репозиториев): `make check` (ruff, mypy, pytest,
|
||||
контракт) и `make check-full` (+ интеграционные тесты против compose, цикл миграций, `check_dbml_vs_orm`) — `docs/dev/checks.md`.
|
||||
- DoD: `GET /api/v1/me` возвращает пользователя и его эффективные права; все миграции применяются и откатываются;
|
||||
|
||||
@ -2,7 +2,7 @@
|
||||
"$schema": "https://json-schema.org/draft/2020-12/schema",
|
||||
"$id": "arm-ka/recognition-request/v1",
|
||||
"title": "Запрос распознавания файла (arm-core → tabular-parser / scan-gateway), контракт v1.0",
|
||||
"description": "Тело сообщения на subject recognition.tabular.request или recognition.image.request; subject должен соответствовать полю pipeline (tabular → recognition.tabular.request, image → recognition.image.request). Заголовок Nats-Msg-Id = <job_id>:<attempt> (не поле тела). Один файл (stored_file) → один job.",
|
||||
"description": "Тело сообщения на subject files.tabular_uploaded или files.img_uploaded; subject должен соответствовать полю pipeline (tabular → files.tabular_uploaded, image → files.img_uploaded). Заголовок Nats-Msg-Id = <job_id>:<attempt> (не поле тела). Один файл (stored_file) → один job.",
|
||||
"$comment": "Версионирование: schema_version = MAJOR.MINOR. MINOR (1.1) — необязательные поля и новые значения enum; MAJOR (2.0) — удаление/переименование полей, смена типов или семантики. Потребитель принимает тот же MAJOR и любой MINOR. Новый MINOR раскатывается сначала на потребителей (ядро и сервисы обновляют копии схем раньше, чем производитель начнёт слать 1.1), потому что закрытые enum отвергают незнакомые значения.",
|
||||
"type": "object",
|
||||
"required": ["schema_version", "job_id", "file_id", "pipeline", "file_type", "original_name", "s3", "reply_deadline", "attempt"],
|
||||
|
||||
@ -2,7 +2,7 @@
|
||||
"$schema": "https://json-schema.org/draft/2020-12/schema",
|
||||
"$id": "arm-ka/recognition-result-message/v1",
|
||||
"title": "Сообщение результата распознавания (tabular-parser / scan-gateway → arm-core), контракт v1.0",
|
||||
"description": "Тело сообщения на subject recognition.result. Заголовок Nats-Msg-Id (не поле тела) на этом subject имеет две формы: ack:<job_id>:<attempt> — для status = processing; result:<job_id>:<attempt> — для done, needs_check и error. Форма запроса <job_id>:<attempt> описана в схеме recognition-request. status = processing — ack «взял в работу», публикуется до чтения файла из S3. group_schema в конверт не входит: источник — файл результата (recognition-result).",
|
||||
"description": "Тело сообщения на subject files.tabular_parsed. Заголовок Nats-Msg-Id (не поле тела) на этом subject имеет две формы: ack:<job_id>:<attempt> — для status = processing; result:<job_id>:<attempt> — для done, needs_check и error. Форма запроса <job_id>:<attempt> описана в схеме recognition-request. status = processing — ack «взял в работу», публикуется до чтения файла из S3. group_schema в конверт не входит: источник — файл результата (recognition-result).",
|
||||
"$comment": "Версионирование: schema_version = MAJOR.MINOR. MINOR (1.1) — необязательные поля и новые значения enum; MAJOR (2.0) — удаление/переименование полей, смена типов или семантики. Потребитель принимает тот же MAJOR и любой MINOR. Новый MINOR раскатывается сначала на потребителей (ядро и сервисы обновляют копии схем раньше, чем производитель начнёт слать 1.1), потому что закрытые enum отвергают незнакомые значения.",
|
||||
"type": "object",
|
||||
"required": ["schema_version", "job_id", "file_id", "status", "service"],
|
||||
|
||||
@ -75,9 +75,9 @@ docker compose --profile services up -d --wait arm-core tabular-parser scan-gate
|
||||
|
||||
| Сервис | Порт хоста (`.env`) | Порт в контейнере | Что делает |
|
||||
|---|---|---|---|
|
||||
| `arm-core` | 8000 (`ARM_CORE_PORT`) | 8000 | ядро: API, outbox/inbox `recognition_job`, подписка `recognition.result` |
|
||||
| `tabular-parser` | 8001 (`TABULAR_PARSER_PORT`) | 8000 | подписка `recognition.tabular.request` (queue group `tabular-parser`) |
|
||||
| `scan-gateway` | 8002 (`SCAN_GATEWAY_PORT`) | 8000 | подписка `recognition.image.request` (queue group `scan-gateway`), горячая папка — том `hotfolder` |
|
||||
| `arm-core` | 8000 (`ARM_CORE_PORT`) | 8000 | ядро: API, outbox/inbox `recognition_job`, подписка `files.tabular_parsed` |
|
||||
| `tabular-parser` | 8001 (`TABULAR_PARSER_PORT`) | 8000 | подписка `files.tabular_uploaded` (queue group `tabular-parser`) |
|
||||
| `scan-gateway` | 8002 (`SCAN_GATEWAY_PORT`) | 8000 | подписка `files.img_uploaded` (queue group `scan-gateway`), горячая папка — том `hotfolder` |
|
||||
| `scan-emulator` | — | — | эмулятор ИС Сканирование: следит за `Input/` тома `hotfolder`, ответы — из `../../fixtures/ocr` (только чтение) |
|
||||
| `arm-core-migrate` | — | — | одноразовый: `alembic upgrade head` и `python -m scripts.seed --dev`, затем `arm-core` стартует |
|
||||
|
||||
@ -211,8 +211,8 @@ dpsql -c "select status, error_code, error_message, published_at::time(3), start
|
||||
`published → processing → error` проходит за десятки миллисекунд (в прогонах 14–51 мс), опросом его не поймать; последовательность видна по
|
||||
`published_at < started_at < finished_at` (`started_at` пишет ack), в логе `arm-core` («Запрос распознавания
|
||||
опубликован» → «Сервис подтвердил получение запроса (ack): задание в processing» → «Результат распознавания принят …
|
||||
error_code=NOT_IMPLEMENTED») и на шине: `recognition.tabular.request` (`Nats-Msg-Id: <job_id>:1`) →
|
||||
`recognition.result` `ack:<job_id>:1` (`status: processing`) → `result:<job_id>:1` (`status: error`). Публикатор
|
||||
error_code=NOT_IMPLEMENTED») и на шине: `files.tabular_uploaded` (`Nats-Msg-Id: <job_id>:1`) →
|
||||
`files.tabular_parsed` `ack:<job_id>:1` (`status: processing`) → `result:<job_id>:1` (`status: error`). Публикатор
|
||||
опрашивает outbox раз в 5 с, поэтому от вставки до публикации проходит до 5 с.
|
||||
|
||||
**2. `image` → эмулятор ИС → `INVALID_DOCUMENT`, сырой ответ в `raw/`:**
|
||||
@ -393,7 +393,7 @@ postgres nats minio`, затем `make test-integration` (или `make check-ful
|
||||
|
||||
> **Не запускайте интеграционные тесты `tabular-parser` и `scan-gateway` при поднятом профиле `services`.** Их
|
||||
> сквозные тесты (`api/tests/integration/test_roundtrip.py`, у `scan-gateway` ещё `test_nats_minio_integration.py`)
|
||||
> публикуют запросы в боевые `recognition.tabular.request` / `recognition.image.request`, а экземпляр сервиса из теста
|
||||
> публикуют запросы в боевые `files.tabular_uploaded` / `files.img_uploaded`, а экземпляр сервиса из теста
|
||||
> подписан в ту же queue group (`tabular-parser` / `scan-gateway`), что и контейнер. NATS отдаёт каждое сообщение группы
|
||||
> одному подписчику — запрос теста может уйти контейнеру, и тест упадёт по таймауту ожидания ack (так прямо сказано в
|
||||
> docstring теста `scan-gateway`). Риск подтверждён чтением кода тестов, тесты при поднятом профиле не запускались.
|
||||
@ -401,7 +401,7 @@ postgres nats minio`, затем `make test-integration` (или `make check-ful
|
||||
> и уникальный subject результатов на тест. Перед тестами сервисов:
|
||||
> `docker compose --profile services stop tabular-parser scan-gateway scan-emulator`; проверить, что чужих подписчиков
|
||||
> нет: `curl -s 'http://127.0.0.1:8222/subsz?subs=1' | grep -o '"subject": *"recognition[^"]*"'` — строк
|
||||
> `recognition.tabular.request` / `recognition.image.request` быть не должно (`recognition.result` — подписка контейнера
|
||||
> `files.tabular_uploaded` / `files.img_uploaded` быть не должно (`files.tabular_parsed` — подписка контейнера
|
||||
> `arm-core`, тестам `arm-core` не мешает).
|
||||
|
||||
## Проверка контракта
|
||||
|
||||
@ -126,7 +126,7 @@ cd api && .venv/bin/python run_server.py
|
||||
Swagger: <http://localhost:8000/docs>. Сервер слушает только `127.0.0.1`, так и оставьте.
|
||||
|
||||
Для сценариев загрузки ОСВ (6.6) нужны ещё `NATS_ENABLED=true` (так в `api/.env.example`) и **tabular-parser**; поднимайте
|
||||
только его, без второго экземпляра ядра (он забирает часть сообщений `recognition.result`):
|
||||
только его, без второго экземпляра ядра (он забирает часть сообщений `files.tabular_parsed`):
|
||||
|
||||
```bash
|
||||
docker compose -f ../docs/dev/docker-compose.yml --profile services up -d --build --no-deps tabular-parser
|
||||
@ -480,7 +480,7 @@ ENV_FILE=.env.test .venv/bin/python -m src.parser.cli "/путь/к/файлу.x
|
||||
PDF и изображения обрабатывает `scan-gateway`; ядро с Среза 3b маппит формы из PDF (`statementDocument`) в документы так же, как из Excel
|
||||
(раздел 6.9), но на стенде это требует профиля `services` с эмулятором, поэтому здесь сервис проверяется отдельно. Сервис и нормализатор проверяются отдельно: поднимите профиль `services`
|
||||
(`scan-gateway` и `scan-emulator` в `docs/dev/docker-compose.yml`) и пройдите сценарий из README `scan-gateway` («Полный круг против compose и эмулятора»:
|
||||
файл в MinIO → `recognition.image.request` в NATS → ack и итоговый конверт).
|
||||
файл в MinIO → `files.img_uploaded` в NATS → ack и итоговый конверт).
|
||||
|
||||
Что загружать на стенде: **PDF из `fixtures/ocr` — `aep_3.pdf` и `aep_6.pdf`**. Эмулятор узнаёт их **по содержимому**
|
||||
(sha256), а не по имени: шлюз кладёт файл в горячую папку как `<uuid>.<ext>`, и имя фикстуры в нём не сохраняется.
|
||||
|
||||
2
openspec/changes/nats-files-subjects/.openspec.yaml
Normal file
2
openspec/changes/nats-files-subjects/.openspec.yaml
Normal file
@ -0,0 +1,2 @@
|
||||
schema: spec-driven
|
||||
created: 2026-10-08
|
||||
49
openspec/changes/nats-files-subjects/design.md
Normal file
49
openspec/changes/nats-files-subjects/design.md
Normal file
@ -0,0 +1,49 @@
|
||||
## Context
|
||||
|
||||
Решение владельца 08.10.2026: subject'ы NATS — как в старом АРМ. На проде заведены `files.img_uploaded`,
|
||||
`files.img_parsed`, `files.tabular_uploaded`, `files.tabular_parsed`; владелец: «можно использовать три топика, давай
|
||||
будем использовать только tabular_parsed. старый арм мы погасим».
|
||||
|
||||
| Поток | Было | Стало |
|
||||
|---|---|---|
|
||||
| запрос `tabular` | `recognition.tabular.request` | `files.tabular_uploaded` |
|
||||
| запрос `image` | `recognition.image.request` | `files.img_uploaded` |
|
||||
| ack и результат (оба сервиса) | `recognition.result` | `files.tabular_parsed` |
|
||||
|
||||
Схема обмена не меняется: два subject'а запроса по `pipeline` и один общий subject ответа, на который ядро подписано
|
||||
queue group `arm-core`. Сопоставление ответа с заданием — по `job_id` и `Nats-Msg-Id`, не по subject.
|
||||
|
||||
## Goals / Non-Goals
|
||||
|
||||
Goals: переименовать subject'ы во всех трёх сервисах и в документах, не меняя тел сообщений и поведения.
|
||||
Non-goals: см. proposal.
|
||||
|
||||
## Decisions
|
||||
|
||||
1. Общий subject ответа — `files.tabular_parsed` для обоих сервисов (прямое указание владельца). Имя исторически
|
||||
«табличное», но смысл у нас — «ответ сервиса распознавания»; это оговорено в `03-architecture.md`.
|
||||
2. Старый АРМ гасится (владелец) — двойной доставки запроса в обработчики старого АРМ (`armka_file_worker`) не будет.
|
||||
|
||||
## Risks / Trade-offs
|
||||
|
||||
- Если старый АРМ не погасят к выкату, его обработчики получат копию каждого запроса (в core NATS каждая queue group
|
||||
получает своё сообщение): файл уйдёт в ИС Сканирование дважды, в `files.tabular_parsed` придут ответы чужого формата
|
||||
(ядро отбросит их по схеме с записью в лог).
|
||||
- Выкат сервисов не по одному: сервис со старыми именами не увидит запросов ядра с новыми.
|
||||
|
||||
## Decisions made without discussion
|
||||
|
||||
1. 08.10.2026 — **R-1:** маршрут — OpenSpec change в каждом из четырёх репозиториев (требования спек называют
|
||||
subject'ы, спека — источник истины), код — по `.bounded` без плана SDD (механическое переименование констант).
|
||||
Цена ошибки: низкая, процессная.
|
||||
2. 08.10.2026 — **R-2:** `schema_version` не меняется. Требование «Версионирование контракта» относится к полям тел
|
||||
сообщений; тела не меняются, subject'ы — часть окружения. Цена ошибки: низкая — потребители не различают версии
|
||||
по subject'у.
|
||||
3. 08.10.2026 — **R-3:** DBML v5.17 не поднимается ради двух заметок (`DECISION-4` в шапке, note колонки
|
||||
`recognition_job.request_subject`): структура не меняется, а новая версия тянет копию модели, `entities`, ссылки в
|
||||
`CLAUDE.md` сервисов и `04-data-model-guide.md`. Заметки обновить в следующей версии DBML — **deferred**. Цена
|
||||
ошибки: до следующей версии заметки в DBML называют старые subject'ы.
|
||||
4. 08.10.2026 — **R-4:** subject'ы остаются константами в `api/src/contract/subjects.py`, в настройки не выносятся
|
||||
(OQ-83 — отдельно). Цена ошибки: смена имён на платформе снова потребует релиза трёх сервисов.
|
||||
5. 08.10.2026 — **R-5:** исторические записи не переписываются: строки OQ-69/OQ-83 в `07-open-questions.md`, архив
|
||||
OpenSpec, планы `docs/superpowers/plans/` сервисов. Цена ошибки: при чтении истории встретятся старые имена.
|
||||
44
openspec/changes/nats-files-subjects/proposal.md
Normal file
44
openspec/changes/nats-files-subjects/proposal.md
Normal file
@ -0,0 +1,44 @@
|
||||
## Why
|
||||
|
||||
На проде платформа завела subject'ы NATS старого АРМ: `files.tabular_uploaded`, `files.img_uploaded`,
|
||||
`files.tabular_parsed`, `files.img_parsed`. Наш контракт публикует запросы в `recognition.{tabular|image}.request`,
|
||||
а ack и результаты — в `recognition.result`; этих subject'ов в правах общего JWT нет. Решение владельца 08.10.2026:
|
||||
subject'ы — «правильно как в старом АРМ», используем три (`files.img_parsed` не используем), старый АРМ гасится.
|
||||
|
||||
Срез: вне плана срезов `09-delivery-plan.md` — подготовка к контуру (инфраструктура NATS платформы).
|
||||
|
||||
## What Changes
|
||||
|
||||
- Запрос `tabular` — subject `files.tabular_uploaded` (было `recognition.tabular.request`).
|
||||
- Запрос `image` — subject `files.img_uploaded` (было `recognition.image.request`).
|
||||
- Ack `processing` и результаты обоих сервисов — общий subject `files.tabular_parsed` (было `recognition.result`).
|
||||
- Тела сообщений, `Nats-Msg-Id`, queue group'ы, `schema_version` и файл результата не меняются.
|
||||
- Документы: `03-architecture.md` (схема, таблица потоков, NATS платформы), `09-delivery-plan.md`, `dev/README.md`,
|
||||
`dev/qa-guide.md`, `repo-templates/*.CLAUDE.md`, описания subject'ов в `contracts/*.schema.json`.
|
||||
|
||||
Читают и пишут контракт: `arm-core` (публикует запросы, подписан на результат), `tabular-parser` и `scan-gateway`
|
||||
(подписаны на свой запрос, публикуют ack и результат). Дочерние change'и `nats-files-subjects`:
|
||||
RSHB/arm-core#77, RSHB/arm-tabular-parser#28, RSHB/arm-scan-gateway#22. Issue этого репозитория — RSHB/arm-ka-docs#74.
|
||||
|
||||
## Non-goals
|
||||
|
||||
- Subject'ы не выносятся в настройки (префикс для тестов — OQ-83, отдельно).
|
||||
- `files.img_parsed` не используется, раздельные subject'ы результата по pipeline не вводятся.
|
||||
- DBML v5.17 не поднимается: заметки `DECISION-4` и `recognition_job.request_subject` обновятся в следующей версии
|
||||
модели (design, решение 3).
|
||||
- `schema_version` контракта не меняется (design, решение 2).
|
||||
|
||||
## Capabilities
|
||||
|
||||
### New Capabilities
|
||||
|
||||
### Modified Capabilities
|
||||
- `recognition-contract`: имена subject'ов запроса и результата.
|
||||
|
||||
## Impact
|
||||
|
||||
- Сервисы: `arm-core`, `tabular-parser`, `scan-gateway` — константы `api/src/contract/subjects.py`, тесты, спеки, README.
|
||||
- Копии схем `api/contracts/*.schema.json` в сервисах обновляются побайтно из `docs/contracts/`.
|
||||
- Выкат: все три сервиса одновременно (старые и новые имена несовместимы на шине). Задания, опубликованные до выката и
|
||||
не получившие ack, ядро повторит уже в новый subject: subject вычисляется по `pipeline` при каждой публикации,
|
||||
`recognition_job.request_subject` — справочное поле.
|
||||
@ -0,0 +1,79 @@
|
||||
## MODIFIED Requirements
|
||||
|
||||
### Requirement: Сообщение запроса распознавания
|
||||
Тело сообщения на 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: Подтверждение взятия в работу (ack без JetStream)
|
||||
Получив корректный запрос (прошедший проверку схемы и `MAJOR`), сервис SHALL немедленно, до чтения файла из S3, опубликовать в `files.tabular_parsed` конверт со
|
||||
`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` в `files.tabular_parsed` появляется конверт `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 `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` может отсутствовать
|
||||
16
openspec/changes/nats-files-subjects/tasks.md
Normal file
16
openspec/changes/nats-files-subjects/tasks.md
Normal file
@ -0,0 +1,16 @@
|
||||
## 1. Контракт (docs)
|
||||
|
||||
- [x] 1.1 `contracts/recognition-request.schema.json`, `contracts/recognition-result-message.schema.json`: subject'ы в описаниях
|
||||
- [x] 1.2 Дельта спеки `recognition-contract` (три требования), Purpose спеки
|
||||
|
||||
## 2. Сервисы (дочерние change'и `nats-files-subjects`)
|
||||
|
||||
- [x] 2.1 arm-core#77: `api/src/contract/subjects.py`, копии схем, тесты, спека `recognition-job-exchange`, README
|
||||
- [x] 2.2 arm-tabular-parser#28: то же, спека `tabular-recognition-service`
|
||||
- [x] 2.3 arm-scan-gateway#22: то же, спека `image-recognition-gateway`
|
||||
|
||||
## 3. Документы контекста
|
||||
|
||||
- [x] 3.1 `context/03-architecture.md`: схема, таблица потоков, NATS платформы (решение владельца 08.10.2026)
|
||||
- [x] 3.2 `context/09-delivery-plan.md`, `dev/README.md`, `dev/qa-guide.md`, `repo-templates/*.CLAUDE.md`
|
||||
- [x] 3.3 `07-open-questions.md` не меняется: исторические строки (design, решение 5)
|
||||
@ -2,7 +2,7 @@
|
||||
|
||||
## Purpose
|
||||
Контракт обмена между ядром `arm-core` и stateless-сервисами распознавания (`tabular-parser`, `scan-gateway`):
|
||||
тела сообщений NATS `recognition.{tabular|image}.request` и `recognition.result`, файл результата в S3 и правила
|
||||
тела сообщений NATS `files.tabular_uploaded`, `files.img_uploaded` и `files.tabular_parsed`, файл результата в S3 и правила
|
||||
версионирования и идемпотентности. Сервисы не знают БД; ядро — единственный писатель (DECISION-4).
|
||||
|
||||
## Requirements
|
||||
|
||||
@ -7,7 +7,7 @@
|
||||
(временный `.part` — в `Tmp`), забрать все `Исходящие/<job_id>/*.json` (по одному json на документ), сохранить склеенный
|
||||
сырой ответ в S3 `raw/{job_id}.json` (в БД не регистрируется), очистить горячую папку (только при `SCAN_CLEANUP=true`),
|
||||
нормализовать в `../docs/contracts/recognition-result.schema.json` (`statementDocument`), опубликовать
|
||||
`recognition.result`. Таймаут 5 мин → `TIMEOUT`. **Нет доступа к БД.**
|
||||
`files.tabular_parsed`. Таймаут 5 мин → `TIMEOUT`. **Нет доступа к БД.**
|
||||
|
||||
## Правила
|
||||
- Нормализация по фактическим ответам `../fixtures/ocr/aep_*.json`, а не по ТЗ (OQ-13, OQ-14): confidence → 0–100,
|
||||
|
||||
@ -4,8 +4,8 @@
|
||||
|
||||
## Что это
|
||||
Stateless-сервис разбора XLS/XLSX/XLSB: ОСВ по счетам (§1 правил) и ББ/ОФР из Excel (§12). Вход — NATS
|
||||
`recognition.tabular.request` + файл в S3 `uploads/`; выход — JSON по `../docs/contracts/recognition-result.schema.json`
|
||||
в S3 `results/` + NATS `recognition.result`. **Нет доступа к БД.** Не знает про организации, периметры, план счетов —
|
||||
`files.tabular_uploaded` + файл в S3 `uploads/`; выход — JSON по `../docs/contracts/recognition-result.schema.json`
|
||||
в S3 `results/` + NATS `files.tabular_parsed`. **Нет доступа к БД.** Не знает про организации, периметры, план счетов —
|
||||
возвращает структуру файла; нормализация и резолв — в arm-core.
|
||||
|
||||
## Главное правило
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user