arm-ka-docs/context/03-architecture.md
Andrey Epifancev ed940e3d73 docs: соглашения шаблона команды, раскладка api/src, схема dkb, контракт NATS v1.0
03-architecture.md: раздел «Соглашения шаблона команды» (раскладка, конфигурация, БД и миграции,
клиенты, DI, тесты, линтеры, сборка, фронт, что вырезано в сервисах распознавания); слои arm-core
переписаны под реальный шаблон (api/src вместо backend/app, web вместо frontend); таблица обмена
через NATS — три схемы контракта v1.0, Nats-Msg-Id, group_schema только в файле результата;
один бакет S3 с префиксами-ролями; схема PostgreSQL dkb. 00-workspace-layout.md: worktree среза
в зеркальном workspace .wt/<срез>/.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
2026-10-03 02:44:14 +04:00

26 KiB
Raw Blame History

03. Архитектура (целевая, по решениям DECISION-1…5 и ARCH-* в DBML v5.13)

Компоненты (= репозитории, см. 00-workspace-layout.md)

                 ┌──────────────┐   JWT (email)   ┌──────────────────────────────┐
  браузер ──────▶│  React SPA   │ ───────────────▶│  arm-core (FastAPI)          │
                 └──────────────┘                 │  REST /api/v1, policy-слой,  │
                                                  │  единственный писатель в БД  │
                                                  └───────┬──────────┬───────────┘
                                                          │          │
                                              PostgreSQL  │          │  S3 (uploads/ results/ exports/ archive/)
                                                          │          │
                        NATS JetStream  ◀─────────────────┘ outbox   │
                   recognition.tabular.request                       │
                   recognition.image.request                         │
                   recognition.result   ───────────────────▶ arm-core (inbox, дедуп по msg_id)
                            │
            ┌───────────────┴───────────────┐
   ┌────────▼────────┐             ┌─────────▼────────┐        ┌──────────────────┐
   │ tabular-parser  │             │ scan-gateway     │ ◀─────▶│ ИС Сканирование  │ (внешняя, горячие папки)
   │ XLS/XLSX/XLSB   │             │ PDF/PNG/JPG      │        └──────────────────┘
   │ stateless, без БД│            │ stateless, без БД│
   └─────────────────┘             └──────────────────┘
  • arm-core — монолитное ядро: справочники, файлы/документы, маппинг результатов распознавания, консолидация (вычисления), согласование, ЕХ, аудит, права. Один Alembic, одна схема.
  • tabular-parser — читает файл из S3 uploads/, находит на листах ОСВ (§1) и формы ББ/ОФР (§12) по содержимому, пишет нормализованный JSON в S3 results/, публикует recognition.result. Контракт результата — docs/contracts/recognition-result.schema.json (черновик; финализировать в срезе 2). Правила распознавания заголовков/группировок — rules/*.yaml.
  • scan-gateway — адаптер ИС Сканирование: переименовать файл UUID_имя.pdf, положить в «Входящие», опросить «Исходящие/UUID/UUID.json», сохранить сырой ответ в S3 raw/{job_id}.json (в БД не регистрируется), нормализовать в тот же контракт результата, опубликовать recognition.result. Таймаут 5 мин → timeout.
  • Frontend — React + TS по шаблону команды; экраны по макету dkb_24.09.26 (справочник ГК, консолидации, первичные документы, проверка распознавания, сводная ОСВ, шахматка, корректировки, гудвилл, КФО, ЕХ, журнал, роли).

Контракт обмена через NATS (DECISION-4)

Схемы — docs/contracts/ (v1.0): recognition-request.schema.json и recognition-result-message.schema.json — тела сообщений, recognition-result.schema.json — файл результата в S3; примеры — docs/contracts/examples/.

Поток 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>
результат recognition.result tabular-parser / scan-gateway schema_version, job_id, file_id, status (done / needs_check / error), error {code, message}, service {name, version}, result_s3 {bucket, key}, result_schema_version, raw_s3_key (только scan-gateway), metrics result:<job_id>:<attempt>

group_schema в конверт не входит — ядро берёт его из documents[].group_schema файла результата при маппинге. Один stream RECOGNITION (subjects recognition.>); durable consumer'ы: tabular-parser и scan-gateway — запросы, arm-core-results — результаты.

Правила:

  • recognition_job создаётся в статусе queued в одной транзакции со stored_file; фоновый публикатор переводит в published (request_msg_id = Nats-Msg-Id). Повтор публикации — по queued старше N секунд.
  • Результат принимается через durable consumer; result_msg_id UNIQUE = дедуп. После max_deliver → dlq; replay = сброс в queued с attempt + 1.
  • Результат одного файла содержит N документов; ядро на каждый создаёт document и маппит данные.
  • Сервисы не знают про БД и про бизнес-смысл: они возвращают структуру файла; нормализация счетов, резолв контрагентов, проверки — в ядре.

Схема ключей S3

Один бакет (OPENBAO__MINIO__MINIO_BUCKET шаблона); роль объекта — первый сегмент ключа: {bucket_role}/{file_id}/{job_id}/{name}. Роли: uploads (исходники, срок — весь срок хранения), results (нормализованные JSON; ретеншн — ARCH-010, открыт), exports (7 дней), archive (data lake). Все объекты регистрируются в s3_object (bucket_role, bucket, key, sha256, size, expires_at). Сырой ответ OCR raw/{job_id}.json — по конвенции, без записи в БД. Lifecycle-правила — по префиксам.

Аутентификация и авторизация

  1. SPA получает JWT от РАИСА; каждый запрос несёт Authorization: Bearer.
  2. arm-core валидирует подпись/срок, извлекает email, находит app_user (is_active); нет пользователя — 403.
  3. Права: effective_permissions = ∪ role_permission(roles(user)). Проверка в зависимости FastAPI require(permission_code).
  4. Policy-слой поверх прав (отдельный модуль, чистые функции): видимость по филиалу (regional_office), по ЗО (business_segment), допустимость действия по статусу версии (consolidation_status) и по активной блокировке (consolidation_lock). Право говорит «может», policy — «эту ли и сейчас ли».
  5. Локальная разработка: заглушка-эмитент JWT с теми же claims (feature-flag AUTH_DEV_ISSUER).

Слои arm-core (по раскладке шаблона команды)

Структура — раскладка шаблона api/src/ (см. «Соглашения шаблона» ниже); tabular-parser и scan-gateway — те же каталоги без web/, db/, models/, alembic/. Соответствие прежним названиям этого документа: backend/app/ → api/src/, integrations/ → core/, migrations/ → api/alembic/, frontend/ → web/.

api/src/
  api/endpoints/     # роутеры FastAPI по контекстам: me, directories, files, documents, consolidation, kfo, ex, audit, users
  api/dependencies/  # фабрики Depends: сессия, сервисы, current_user, require(permission)
  domain/            # чистая предметная логика без I/O: правила ОСВ, периоды, матчинг, элиминация, проверки, пороги,
                     # машины статусов, policy — pydantic-модели на входе и выходе
  services/          # use-cases: оркестрация domain + repositories + outbox + audit_event
  repositories/      # SQLAlchemy-запросы, в т. ч. запросы-вычисления (сводная ОСВ, шахматка)
  models/            # ORM 1:1 с DBML v5.13; имена таблиц/колонок/enum — как в DBML
  schemas/           # pydantic DTO API /api/v1
  core/              # config, clients (S3; NATS outbox-publisher и result-consumer), auth (JWT РАИСА / dev-эмитент),
                     # logger, exception_handlers
  db/                # engine, сессия
api/alembic/         # миграции
api/scripts/         # check_dbml_vs_orm.py, seed
api/tests/           # фикстуры — из ../fixtures (ARM_KA_FIXTURES)
web/                 # SPA
openspec/

Принципы: domain/ тестируется без БД на фикстурах из ../fixtures; вычисляемые объекты консолидации возвращаются как pydantic-модели и не имеют ORM-таблиц; все мутации идут через services/ с записью audit_event.

Статусы и машины состояний

Три независимые машины: file_status (техника файла), document_status (проверка аналитиком), consolidation_status (маршрут КФО) + recognition_status (жизненный цикл задания). Переходы — 05-domain-rules.md §9. Реализовать как явные таблицы переходов в domain/, а не if-ы по роутерам.

Соглашения шаблона команды

Три сервисных репозитория развёрнуты из нулевого коммита репозитория команды RSHB/DKB_ARMKA (ba172f8, «Шаблон FastAPI» платформы РАИСА/aurora): дерево импортировано как есть (arm-core@ba92222), доменный код удалён, каркас оставлен (8e1f423). Документы подстраиваются под шаблон, а не наоборот. Ниже — что шаблон задаёт и что мы к нему добавляем. Один и тот же текст действует для всех трёх репозиториев; отличия сервисов — в разделе «Что вырезано».

Раскладка репозитория

<repo>/
  README.md               описание шаблона (в Срезе 0 заменяется описанием сервиса)
  raisa_config.yml        профиль ресурсов платформы (cpu/ram); читает платформа при деплое
  setup/                  бутстрап разработчика: setup.sh (venv + pip + npm i); pip.conf и .npmrc → внутренний Nexus банка
  api/                    backend. РАБОЧИЙ КАТАЛОГ для всех команд: от него ищутся .env, pytest.ini и web/ для раздачи SPA
    run_server.py         точка входа платформы: uvicorn, host/port из SERVER__HOST / SERVER__PORT, lifespan=on
    requirements.txt      прод-зависимости, pinned (fastapi 0.135, SQLAlchemy 2.0.36, alembic 1.14, asyncpg, pydantic 2.13,
                          pydantic-settings, nats-py 2.14, miniopy-async, smbprotocol, loguru, sentry-sdk, raisa_fastapi_protected_api)
    requirements-dev.txt  dev-зависимости (pytest-asyncio, pytest-env, openpyxl, …) — пины рассинхронизированы с requirements.txt
    pytest.ini            asyncio_mode=auto, loop scope session, env APP_ENV=test / ENV_FILE=.env.test, testpaths=tests
    alembic.ini, alembic/ env.py берёт URL из настроек приложения, схема dkb, compare_type=True, include_schemas
    src/
      __init__.py         экспортирует app и get_settings
      app.py              FastAPI: lifespan (SMB-сессия, MinIO, NATS), middleware (gzip, proxy headers, CORS, РАИСА при DEBUG=false),
                          GET /healthcheck, include_router, раздача SPA из api/web/ (SPAStaticFiles, fallback index.html)
      api/router.py       агрегирующий APIRouter; api/endpoints/* — роутеры; api/dependencies/* — фабрики Depends
      core/config.py      pydantic-settings (см. «Конфигурация»); модуль создаёт settings при импорте
      core/clients.py     клиенты инфраструктуры (S3Storage поверх miniopy-async)
      core/nats_callback.py  обработчики сообщений NATS
      core/logger.py      loguru: setup_logger, перехват std logging и uvicorn, request_id, шумовые пути
      core/exception_handlers.py  ошибки → JSON {"detail": …}
      common/exceptions.py  доменные исключения (NotFoundError, CouldntCreateError)
      db/db.py            async engine (asyncpg), search_path=dkb, get_async_session
      models/base.py      DeclarativeBase с MetaData(schema="dkb"), UUIDPrimaryMixin (uuid4 на стороне приложения)
      schemas/            pydantic DTO и enum'ы API
      repositories/       запросы: класс на таблицу, принимает AsyncSession
      services/           use-cases
    tests/conftest.py
  web/                    frontend (остаётся только в arm-core): React 18 + TS, rsbuild, antd, Redux Toolkit, biome

Наши дополнения к api/src/, не меняющие существующих каталогов: domain/ — чистая предметная логика без I/O (правила §1–§10, машины статусов, policy); core/auth.py — JWT РАИСА и dev-эмитент; api/scripts/ — служебные скрипты (check_dbml_vs_orm.py, seed). Пути в документах вида rules/*.yaml, scripts/…, tests/… отсчитываются от api/.

Конфигурация

  • pydantic-settings, файл api/.env, вложенность через __, лишние переменные игнорируются. Все инфраструктурные секреты — под префиксом OPENBAO (в проде заполняет OpenBao): OPENBAO__DATABASE__DATABASE_URL, OPENBAO__MINIO__{MINIO_URL, MINIO_ADMIN_LOGIN, MINIO_ADMIN_PASSWORD, MINIO_BUCKET}, OPENBAO__NATS__{NATS_URL, NATS_SEED, NATS_JWT}, OPENBAO__SMB__{SMB_HOST, SMB_PATH, SMB_LOGIN, SMB_PASSWORD}. Прочее: DEBUG, ROOT_PATH, VERSION, SERVER__HOST, SERVER__PORT.
  • settings создаётся при импорте (core/config.py, db/db.py, app.py) — импорт почти любого модуля требует полного окружения; тесты получают его из .env.test.
  • DEBUG=true — режим разработки шаблона: NATS не подключается, middleware РАИСА не импортируется. Наши флаги разработки (AUTH_DEV_ISSUER, SCAN_EMULATOR, режим горячих папок) добавляются так же — отдельными полями Settings, по умолчанию выключены; в проде ни один не включается.

База данных и миграции

  • SQLAlchemy 2.0 async + asyncpg. Одна схема PostgreSQL dkb — зашита в шаблоне (MetaData, search_path, version_table_schema и include_name Alembic). Следуем шаблону, не public.
  • Alembic берёт URL из настроек, не из alembic.ini; ревизии — api/alembic/versions/, имя файла стандартное <rev>_<slug>.py. Автогенерация — только черновик: итоговую ревизию сверяет с DBML api/scripts/check_dbml_vs_orm.py.
  • PK везде uuid через UUIDPrimaryMixin; имена таблиц, колонок и enum'ов — ровно как в DBML v5.13.

Инфраструктурные клиенты

  • S3 — miniopy-async (Minio, cert_check=False), один бакет MINIO_BUCKET; роли s3_object.bucket_role становятся первым сегментом ключа (uploads/…, results/…, raw/…, exports/…, archive/…).
  • NATS — nats-py; аутентификация nkeys-seed + JWT (NATS_SEED, NATS_JWT) через user_jwt_cb/signature_cb; соединение и подписки создаются в lifespan и живут в app.state.nats / app.state.nats_subs. JetStream (durable consumer, Nats-Msg-Id) — поверх того же соединения.
  • SMB (горячие папки ИС Сканирование) — smbprotocol/smbclient, сессия регистрируется в lifespan; нужен только scan-gateway.
  • Внешняя аутентификация — пакет raisa_fastapi_protected_api из внутреннего Nexus (AuthorizationMiddleware, ProtectedSettings, …), импортируется только при DEBUG=false; фронтовая пара — raisa-fastapi-protected-web. Вне сети банка оба пакета недоступны (см. вопросы Среза 0).

DI, слои, ошибки, логи

  • Контейнера DI нет: ручные фабрики Depends в api/dependencies/ — фабрика получает AsyncSession из get_async_session, собирает репозитории и сервис; инфраструктурные клиенты берёт из request.app.state (storage, nats).
  • Поток запроса: api/endpoints → services → repositories → models; DTO — schemas/. Доменные исключения из common/exceptions.py маппятся в HTTP в core/exception_handlers.py (RequestValidationError → 422, IntegrityError → 409, SQLAlchemyError → 500, HTTPException как есть, прочее → 500); тело всегда {"detail": …}, тексты по-русски.
  • Логи — loguru (core/logger.setup_logger): перехват std logging и uvicorn, request_id в extra, успешные обращения к /healthcheck, /docs, статике не логируются.

Тесты

  • pytest + pytest-asyncio (asyncio_mode=auto, loop scope session) + pytest-env; запуск из api/; тесты в api/tests/.
  • Тестовую БД шаблон не задаёт. Решение: тесты, которым нужна БД, идут на PostgreSQL из docker-compose (enum'ы, partial unique, jsonb, NULLS NOT DISTINCT в SQLite не воспроизводятся); domain/ тестируется без БД.

Линтеры и типизация

  • Backend: шаблон декларирует black / isort / pylint (бейджи README), конфигов не содержит. Принимаем ruff (format — совместим с black; lint с правилами isort I и подмножеством pylint PL) и mypy; конфиг — api/pyproject.toml (решение Среза 0; откат на black/isort/pylint — замена одного файла конфига и шага CI).
  • Frontend: biome 1.9 (npx biome check src): ширина строки 140, одинарные кавычки, bracketSameLine, noExplicitAny warn, useAwait/noEmptyBlockStatements error; типы — npx tsc --noEmit (strict). ESLint/Prettier нет.

Сборка и деплой

  • Платформа РАИСА запускает api/run_server.py с SERVER__HOST/SERVER__PORT из окружения; ресурсы — raisa_config.yml.
  • Dockerfile, docker-compose.yml, .dockerignore шаблон держит в .gitignore («Custom ignore»): образ собирает платформа. Для локальной разработки и CI: compose — на уровне workspace (docs/dev/docker-compose.yml), Dockerfile — в корне каждого сервиса, строка Dockerfile убирается из .gitignore (единственная правка шаблона; решение Среза 0).
  • Фронт: npm run build (rsbuild) → web/dist/ → копируется в api/web/, откуда FastAPI раздаёт SPA; роутеры подключаются до mount'а /.
  • Зеркала пакетов: pip и npm настроены на nexus.dev.raisa.go.rshbank.ru (setup/pip.conf, setup/.npmrc).
  • CI-конфига в шаблоне нет; шаблон пайплайна — docs/dev/ci/ (change slice-0-skeleton в docs).

Frontend (только arm-core)

  • React 18.3 + TypeScript 5.7 (strict), сборка rsbuild 1.6 (npm run start | build | preview); основной UI-кит — antd 6 (+ @ant-design/icons); установлены также tailwind 3 + daisyui 4 (тема rshb), react-daisyui, raisa-ui — точечно; состояние — Redux Toolkit 2 (store/store.ts, слайсы в store/slices/, типизированные useAppDispatch/useAppSelector); маршрутизация — react-router-dom 7 (BrowserRouter basename=Config.APP_ADDRESS, маршруты в App.tsx); pdfjs-dist — PDF.
  • Структура web/src/: layouts/ — страницы, widgets/ — составные блоки, components/ — примитивы, store/, conf/; стили — CSS-модули (styles.module.css) и токены antd (ConfigProvider в index.tsx).
  • Переменные окружения только с префиксом REACT_APP_ (rsbuild loadEnv), читаются через conf/config.ts: все запросы к бэку — ${Config.API_URL}/api/v1/… (в сборке относительный путь); REACT_APP_ROOT_PATH = ROOT_PATH бэка (по умолчанию /aurora/apps/fastapi-dkb-armka) задаёт base/assetPrefix rsbuild и basename роутера.

Что вырезано в tabular-parser и scan-gateway

Сервисы распознавания — тот же шаблон без фронта и без БД (DECISION-4). На момент Среза 0 оба репозитория — полные копии каркаса arm-core; change slice-0-skeleton каждого сервиса удаляет: web/ целиком; setup/.npmrc и npm-шаги setup/setup.sh; из app.py — mount SPA (SPAStaticFiles, os.makedirs("web")), CORS, импорт raisa_fastapi_protected_api (внешнего API нет — только /healthcheck); db/, models/, repositories/, alembic/, alembic.ini и OPENBAO.DATABASE из настроек; из requirements.txt — asyncpg, SQLAlchemy, alembic, argon2-cffi, raisa_fastapi_protected_api; шаблонный README.md заменяется описанием сервиса. tabular-parser дополнительно без SMB (smbprotocol, OPENBAO.SMB, регистрация сессии в lifespan). Остаются: run_server.py, raisa_config.yml, setup/setup.sh

  • pip.conf, pytest.ini, настройки OPENBAO__MINIO__* / OPENBAO__NATS__* / DEBUG, core/config.py, core/clients.py, core/nats_callback.py, core/logger.py, core/exception_handlers.py, common/exceptions.py. Точный список — в CLAUDE.md сервиса и в tasks.md его change'а.

Что ожидаем от платформы (для запроса ресурсов)

PostgreSQL 15+ (одна БД, схема dkb — как в шаблоне), NATS JetStream (один stream RECOGNITION, 3 subject; аутентификация nkeys/JWT, как в шаблоне), S3-совместимое хранилище (один бакет; префиксы-роли + lifecycle по префиксам), Nexus (pip/npm, пакеты raisa_*), Gitea + CI (lint, pytest, миграции с нуля, сборка образов), реестр образов. Секреты — через переменные окружения (OPENBAO__*).