155 lines
22 KiB
Markdown
155 lines
22 KiB
Markdown
# Design: Срез 0 — сквозной скелет
|
||
|
||
## Context
|
||
|
||
См. `proposal.md` — Why. Ограничения, формирующие решения:
|
||
|
||
- Шаблон команды (`03-architecture.md`, «Соглашения шаблона команды»): один бакет S3, NATS с nkeys/JWT, настройки
|
||
`OPENBAO__*`, `DEBUG=true` отключает внешние интеграции, `Dockerfile`/`docker-compose.yml` в `.gitignore` шаблона.
|
||
- DECISION-4 DBML: сервисы распознавания без БД; обмен только через `recognition_job` (outbox/inbox), дедуп по `msg_id`, DLQ.
|
||
- **Факты платформы (ответы пользователя 03.10.2026):** Python **3.11**; версия Node неизвестна; доступа к Nexus банка
|
||
(`raisa_*`) не будет до деплоя в прод; CI в Gitea нет и не планируется — у платформы свой CI/CD, в него не лезем;
|
||
**NATS без JetStream**; PostgreSQL **16**; создавать схемы можно; параметры JWT РАИСА пока неизвестны.
|
||
- Фикстуры — отдельный приватный репозиторий `arm-ka-fixtures`; локально доступен как `../fixtures`.
|
||
|
||
## Goals / Non-Goals
|
||
|
||
**Goals:** один файл контракта на каждое сообщение; один `docker compose up` для всего workspace; одна команда проверки
|
||
качества, одинаковая для трёх репозиториев; документы контекста соответствуют реальному шаблону и реальной платформе.
|
||
|
||
**Non-Goals:** см. proposal. Дополнительно: не проектируем prod-деплой (это платформа банка), не решаем ретеншн `results/`
|
||
(ARCH-010), не пишем конфигурации CI ни для Gitea, ни для платформы.
|
||
|
||
## Decisions
|
||
|
||
### D-1. Контракт — JSON Schema в docs + вендоренная копия в каждом сервисе, без общего пакета
|
||
`09-delivery-plan.md` оставлял выбор: пакет `arm-ka-contracts` или копия с проверкой идентичности. Выбрана **копия**:
|
||
`docs/contracts/*.schema.json` — источник истины; каждый сервис хранит копию в `api/contracts/` и pydantic-модели в
|
||
`api/src/contract/`; тест сервиса сравнивает копию с `../docs/contracts` байт в байт (пропускается с предупреждением,
|
||
если `docs` недоступен и не задан `ARM_KA_DOCS`), а pydantic-модели валидируют `docs/contracts/examples/*.json` и
|
||
генерируют JSON, проходящий схему. *Альтернатива* — публикуемый пакет: нет внутреннего PyPI, добавляет цикл релиза ради
|
||
трёх потребителей. *Цена ошибки:* при переходе на пакет копии удаляются, тест идентичности заменяется пином — полдня.
|
||
|
||
### D-2. Конверт результата не дублирует `group_schema`
|
||
`03-architecture.md` перечислял `group_schema` в сообщении `recognition.result`. В конверте остаются только поля для
|
||
маршрутизации и статуса; `group_schema` ядро берёт из `documents[].group_schema` файла результата при маппинге
|
||
(Срез 2). *Почему:* одна истина на результат, сообщение остаётся маленьким (лимит NATS 1 МБ не трогаем).
|
||
|
||
### D-3. Доставка без JetStream: app-level ack, повторная публикация ядром, дедуп в БД
|
||
JetStream на платформе нет, значит нет durable consumer'ов, серверного дедупа по `Nats-Msg-Id`, redelivery и `max_deliver`.
|
||
Схема на core NATS:
|
||
- Запросы публикуются ядром из outbox (`recognition_job.status = queued → published`); сервисы подписаны **queue group**
|
||
(`tabular-parser`, `scan-gateway`) — несколько инстанций делят нагрузку, одно сообщение получает одна.
|
||
- Сервис **сразу по получении** публикует в `recognition.result` конверт со `status = processing` (ack: «взял в работу») —
|
||
это значение уже есть в `recognition_status` DBML («сервис подтвердил взятие в работу»). Ядро ставит `processing`,
|
||
`started_at`, `service_name/version`.
|
||
- Если ack не пришёл за `ACK_TIMEOUT_SECONDS` (30) — ядро **публикует запрос повторно** с тем же `Nats-Msg-Id`
|
||
(`<job_id>:<attempt>`, тот же `attempt`: попытка обработки не началась), не более `OUTBOX_MAX_REPUBLISH` (3) раз, затем
|
||
`status = dlq` (DLQ-поведение DBML без брокера). `attempt`/`max_attempts` остаются для replay после `error`/`timeout`
|
||
(ручной, Срез 3+).
|
||
- Результат, потерянный при простое ядра, обнаруживается по `reply_deadline` → `timeout`; `result_msg_id UNIQUE` в
|
||
`recognition_job` остаётся единственным дедупом результатов (`Nats-Msg-Id` сервисы проставляют сами).
|
||
- Сервис держит process-local множество `job_id` в работе: повторный запрос с тем же `job_id` (пришёл повторно, пока
|
||
первый обрабатывается или уже обработан в этом процессе) подтверждается ack'ом и не обрабатывается второй раз.
|
||
*Альтернатива* — ждать JetStream или ставить свою очередь (Redis/БД-поллинг у сервисов): первое не в нашей власти, второе
|
||
нарушает DECISION-4 (сервисы без БД). *Если JetStream появится:* меняются только publisher/subscriber (`js.publish`,
|
||
durable consumer), outbox/inbox и контракт остаются.
|
||
|
||
### D-4. Subjects и группы
|
||
Stream'ов и consumer'ов нет. Subjects: `recognition.tabular.request`, `recognition.image.request`, `recognition.result`.
|
||
Queue group'ы: `tabular-parser`, `scan-gateway` — на запросах; `arm-core` — на результатах (несколько инстанций ядра
|
||
получают результат по одному разу). Заголовки `Nats-Msg-Id` ставятся всегда (core NATS 2.2+ поддерживает заголовки) —
|
||
для трассировки и будущего JetStream.
|
||
|
||
### D-5. Один бакет `arm-ka`, роли — префиксы
|
||
Следствие шаблона (`MINIO_BUCKET`). Ключи: `uploads/{file_id}/{job_id}/{original_name}`,
|
||
`results/{file_id}/{job_id}/result.json`, `raw/{job_id}.json`, `exports/…`, `archive/…`. Lifecycle в MinIO dev не
|
||
настраиваем; в проде — по префиксам (ARCH-010 для `results/`).
|
||
|
||
### D-6. Эмулятор ИС Сканирование — часть `scan-gateway`, не отдельный репозиторий
|
||
Эмулятор знает формат горячих папок и ответы Content AI так же, как сам шлюз; живёт в `scan-gateway/api/src/scan_emulator/`,
|
||
в compose запускается тем же образом другой командой с общим volume `hotfolder` и смонтированным `../../fixtures/ocr`.
|
||
*Альтернатива* — скрипт в `docs/dev/`: пришлось бы дублировать соглашения об именах `UUID_имя` и структуре `Output/UUID/UUID.json`.
|
||
|
||
### D-7. Compose на уровне workspace, Dockerfile — в каждом сервисе
|
||
`docs/dev/docker-compose.yml` собирает сервисы из `../../<repo>` (работает и из `.wt/<срез>/docs/dev`). Для этого из
|
||
`.gitignore` сервисов убирается строка `Dockerfile` (единственная правка шаблона); `docker-compose.yml` в репозиториях
|
||
не появляется. Образ для прода собирает платформа по своим правилам — наш `Dockerfile` только для dev и `make check-full`.
|
||
|
||
### D-8. Проверки качества — локальная команда, без конфигураций CI
|
||
CI в Gitea нет, пайплайн платформы — вне репозиториев. Каждый репозиторий получает одинаковые цели `Makefile`:
|
||
`make check` (быстро, без Docker: `ruff check`, `ruff format --check`, `mypy`, `pytest -m "not integration"`, тест
|
||
идентичности контракта) и `make check-full` (`make check` + интеграционные тесты против compose; в `arm-core` — ещё
|
||
«`alembic upgrade head` → `downgrade base` → `upgrade head`» и `check_dbml_vs_orm.py`). `make check-full` обязателен
|
||
перед PR, результат вписывается в описание PR. Правила — `docs/dev/checks.md`. Фикстуры берутся из `../fixtures` или
|
||
`ARM_KA_FIXTURES`; при отсутствии тесты с маркером `fixtures` пропускаются с предупреждением. Что из этого попадёт в
|
||
пайплайн платформы — решает платформа; мы даём ей готовые команды.
|
||
|
||
### D-9. Версии инструментов
|
||
Python **3.11** (`python:3.11-slim` в Dockerfile; пины `requirements.txt` совместимы). PostgreSQL **16** в compose
|
||
(код не использует ничего новее 15; `NULLS NOT DISTINCT` доступен). NATS **2.10 без `--jetstream`** — паритет с прод.
|
||
Node — неизвестен; локально `node:22` LTS, только для сборки `web/` в compose; меняется одной строкой.
|
||
|
||
### D-10. Пакеты `raisa_*` без Nexus
|
||
`raisa_fastapi_protected_api` остаётся в `requirements.txt` для сборки платформой; dev-установка (`make venv`,
|
||
`Dockerfile`) ставит `requirements.txt` без строк `raisa_*` (фильтр `grep -v`), код импортирует пакет условно, как в
|
||
шаблоне (`DEBUG=false`). В `web/package.json` `raisa-ui` и `raisa-fastapi-protected-web` переносятся в
|
||
`optionalDependencies`: `npm install` без Nexus проходит, с Nexus — ставит их как раньше. *Цена ошибки:* если платформа
|
||
требует их в `dependencies` — вернуть две строки.
|
||
|
||
## Risks / Trade-offs
|
||
|
||
- [Без JetStream сообщение теряется при простое получателя] → запросы: ack + повторная публикация ядром (D-3);
|
||
результаты: `reply_deadline` → `timeout` и replay; для OCR повторная публикация до ack не повторяет распознавание
|
||
(сервис ещё не брал файл).
|
||
- [Повторный запрос пришёл в сервис, который уже обрабатывает `job_id`] → process-local множество в работе; имя файла в
|
||
горячей папке = `job_id`, повторный `put` перезаписывает тот же файл.
|
||
- [Публичные образы PostgreSQL/NATS/MinIO отличаются от банковских] → версии зафиксированы по ответам (16 / 2.10 без JS).
|
||
- [Шаблон инициализирует nkeys при импорте `app.py`] → задача сервисам: пустые `NATS_SEED/NATS_JWT` = соединение без
|
||
учётных данных (dev); при заданных — как в шаблоне.
|
||
- [Копии контракта расходятся] → тест идентичности в `make check` каждого сервиса.
|
||
- [Эмулятор отвечает только для файлов, у которых есть пара `aep_N.json` (сейчас две)] → для остальных имён —
|
||
ответ с `error.code = INVALID_DOCUMENT` в формате Content AI; это тоже проверяемый сценарий.
|
||
- [Нет CI — проверки можно забыть] → `make check-full` в DoD каждой задачи и в описании PR; ревьюер требует вывод.
|
||
|
||
## Migration Plan
|
||
|
||
Нет данных для миграции. Порядок внедрения: контракт → compose инфраструктуры → arm-core → сервисы → compose профиль
|
||
`services` → проверки качества → документы. Откат — удаление ветки; `docs/contracts/recognition-result.schema.json`
|
||
остаётся совместимым с текущим черновиком (добавлен только код ошибки).
|
||
|
||
## Open Questions
|
||
|
||
Отвечено 03.10.2026: Python 3.11; Nexus недоступен до прода; CI в Gitea нет, пайплайн платформы не трогаем; JetStream нет;
|
||
PostgreSQL 16, схемы создавать можно. Остаются (не блокируют старт):
|
||
|
||
1. Версия Node для `web/` (пока `node:22`).
|
||
2. JWT РАИСА: алгоритм, источник ключей, имена claim'ов, issuer/audience — нужны к первому деплою; до этого dev-эмитент.
|
||
3. Имена входящего/исходящего каталогов и путь SMB на стенде ИС Сканирование — к Срезу 3.
|
||
4. Появится ли JetStream на платформе позже — влияет только на publisher/subscriber (D-3).
|
||
|
||
## Decisions made without discussion
|
||
|
||
| № | Дата | Решение | Обоснование | Цена ошибки |
|
||
|---|---|---|---|---|
|
||
| 1 | 2026-10-03 | D-1: контракт вендорится копией, общего пакета нет | нет внутреннего PyPI; три потребителя | полдня на переход к пакету |
|
||
| 2 | 2026-10-03 | D-2: `group_schema` убран из конверта результата | одна истина в файле результата | правка одной таблицы в docs и одной pydantic-модели |
|
||
| 3 | 2026-10-03 | D-3/D-4: без JetStream — ack `processing`, повторная публикация ядром (30 с, 3 раза), queue group'ы, дедуп по `result_msg_id` | ответ пользователя «JetStream нет»; DECISION-4 требует не трогать БД из сервисов | при появлении JetStream — замена publisher/subscriber |
|
||
| 4 | 2026-10-03 | D-5: один бакет `arm-ka`, роли как префиксы | шаблон команды (`MINIO_BUCKET`) | `s3_object.bucket` хранится отдельно — смена на несколько бакетов не ломает модель |
|
||
| 5 | 2026-10-03 | D-6: эмулятор ИС Сканирование живёт в `scan-gateway` | общие соглашения горячих папок | перенос каталога |
|
||
| 6 | 2026-10-03 | D-7: compose в `docs/dev/`, `Dockerfile` разыгнорирован в сервисах | шаблон игнорирует Dockerfile, платформа собирает сама | вернуть строку в `.gitignore` |
|
||
| 7 | 2026-10-03 | D-8: проверки качества — `make check` / `make check-full`, конфигураций CI нет | CI в Gitea нет, пайплайн платформы не наш | добавить YAML под нужный раннер, команды уже есть |
|
||
| 8 | 2026-10-03 | В контракт v1.0 добавлен код ошибки `NOT_IMPLEMENTED` и статус конверта `processing` (ack) | заглушки Среза 0; доставка без JetStream | удалить значения enum в 1.1 |
|
||
| 9 | 2026-10-03 | Справочники продукта для seed — в `arm-core/api/seed/`, не в `fixtures/directories/` | данные продукта нужны в проде; `fixtures` — приватные тестовые данные | перенос файлов и правка §6 `06-test-data.md` |
|
||
| 10 | 2026-10-03 | D-9: `python:3.11-slim`, PostgreSQL 16, NATS 2.10 без JS, `node:22` до ответа | ответы пользователя; Node неизвестен | смена тега в compose/Dockerfile |
|
||
| 11 | 2026-10-03 | Worktree среза — зеркальный workspace `.wt/<срез>/` с симлинками | относительные пути `../docs`, `../fixtures` и хуки должны работать в ветке | переместить worktree |
|
||
| 12 | 2026-10-03 | D-10: `raisa_*` фильтруются при dev-установке, npm-пакеты `raisa-*` → `optionalDependencies` | Nexus недоступен до прода | вернуть две строки в `dependencies` |
|
||
| 13 | 2026-10-03 | Файл результата: пустой `documents` допустим только при `status = error` — выражено в схеме условием `if/then` (`minItems: 1`) | требование spec `recognition-contract` «Файл результата»; текст задачи 1.1 его не перечислял | ослабить/убрать условие в схеме, пересобрать копии контракта в сервисах |
|
||
| 14 | 2026-10-03 | Compose: фиксированное имя проекта `name: arm-ka`, порты хоста переопределяются через `.env` со значениями по умолчанию из spec | одинаковые тома/сети из любого worktree; порты 5432/4222 на машине разработчика могут быть заняты | смена имени проекта — потеря dev-томов (`down -v`), порты — правка `.env` |
|
||
| 15 | 2026-10-03 | Чекбоксы плана и `tasks.md` отмечает контроллер SDD отдельным коммитом после чистого ревью задачи | исполнитель не должен отмечать выполненным то, что ещё не прошло ревью | отметки правятся вручную |
|
||
| 16 | 2026-10-03 | Compose: MinIO берётся из неофициальных образов `alpine/minio:RELEASE.2025-10-15T17-29-55Z` и `bitnamilegacy/minio-client:2025.7.21` | `minio/minio` и `minio/mc` недоступны на Docker Hub (denied), на quay.io нужных тегов нет; для dev достаточно любого S3-совместимого MinIO нужной версии API | образы сторонние: при появлении доступа к банковскому реестру/официальным образам — заменить две строки `image:` в `dev/docker-compose.yml`; в прод эти образы не попадают |
|
||
| 17 | 2026-10-03 | Compose: сервис `minio` запускается от `user: "0:0"` | образ `alpine/minio` по умолчанию работает от пользователя `minio` и падает на свежем именованном томе | только dev; удалить строку, когда образ/том позволят писать без root |
|
||
| 18 | 2026-10-03 | `dev/.env.example` дополнен заглушками `OPENBAO__SMB__*` и `NATS_ENABLED=true` | `Settings` шаблона (arm-core) требует блок `OPENBAO.SMB`, без него сервис из venv не стартует; `NATS_ENABLED` по умолчанию `false`, и `/healthcheck` показывает `nats = off`; spec велит копировать именно `docs/dev/.env.example` | убрать две группы строк; сервисы с другим набором обязательных настроек получат лишние (игнорируемые) переменные |
|
||
| 19 | 2026-10-03 | `schema_version` во всех трёх схемах: `const "1.0"` заменён на `type: string, pattern: ^1\.\d+$` (производитель v1.0 пишет `"1.0"`); в `$comment` и в 03-architecture.md добавлено правило «новый MINOR раскатывается сначала на потребителей» | spec «Версионирование контракта»: потребитель принимает тот же MAJOR и любой MINOR, сервис «поддерживающий 1.x» принимает 1.1 и отвергает только 2.0; с `const` это неисполнимо, а копии схем уйдут в сервисы побайтно (финальное ревью I-1) | вернуть `const "1.0"` и пересобрать копии схем в сервисах; побочный риск — копии в `arm-core`/`tabular-parser`/`scan-gateway` нужно обновить (тест идентичности контракта упадёт, пока копии не обновлены) |
|
||
| 20 | 2026-10-03 | Compose: порты хоста публикуются на `${BIND_ADDR:-127.0.0.1}`, а не на 0.0.0.0 | пароли по умолчанию (`armka`, `minioadmin`); все сценарии идут через localhost; spec bind-адрес не оговаривает (финальное ревью M-6) | если нужен доступ с другой машины — `BIND_ADDR=0.0.0.0` в `.env`; для запуска профиля `services` порты хоста не нужны |
|