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

18 KiB
Raw Blame History

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