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>
26 KiB
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 в S3results/, публикуетrecognition.result. Контракт результата —docs/contracts/recognition-result.schema.json(черновик; финализировать в срезе 2). Правила распознавания заголовков/группировок —rules/*.yaml. - scan-gateway — адаптер ИС Сканирование: переименовать файл
UUID_имя.pdf, положить в «Входящие», опросить «Исходящие/UUID/UUID.json», сохранить сырой ответ в S3raw/{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-правила — по префиксам.
Аутентификация и авторизация
- SPA получает JWT от РАИСА; каждый запрос несёт
Authorization: Bearer. - arm-core валидирует подпись/срок, извлекает email, находит
app_user(is_active); нет пользователя — 403. - Права:
effective_permissions = ∪ role_permission(roles(user)). Проверка в зависимости FastAPIrequire(permission_code). - Policy-слой поверх прав (отдельный модуль, чистые функции): видимость по филиалу (
regional_office), по ЗО (business_segment), допустимость действия по статусу версии (consolidation_status) и по активной блокировке (consolidation_lock). Право говорит «может», policy — «эту ли и сейчас ли». - Локальная разработка: заглушка-эмитент 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_nameAlembic). Следуем шаблону, неpublic. - Alembic берёт URL из настроек, не из
alembic.ini; ревизии —api/alembic/versions/, имя файла стандартное<rev>_<slug>.py. Автогенерация — только черновик: итоговую ревизию сверяет с DBMLapi/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 scopesession) + 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 с правилами isortIи подмножеством pylintPL) иmypy; конфиг —api/pyproject.toml(решение Среза 0; откат на black/isort/pylint — замена одного файла конфига и шага CI). - Frontend:
biome1.9 (npx biome check src): ширина строки 140, одинарные кавычки,bracketSameLine,noExplicitAnywarn,useAwait/noEmptyBlockStatementserror; типы —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/(changeslice-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_(rsbuildloadEnv), читаются черезconf/config.ts: все запросы к бэку —${Config.API_URL}/api/v1/…(в сборке относительный путь);REACT_APP_ROOT_PATH=ROOT_PATHбэка (по умолчанию/aurora/apps/fastapi-dkb-armka) задаётbase/assetPrefixrsbuild и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__*).