From ed940e3d730737482a22a4acfaefc48c1832b4b8 Mon Sep 17 00:00:00 2001 From: Andrey Epifancev Date: Sat, 3 Oct 2026 02:44:14 +0400 Subject: [PATCH] =?UTF-8?q?docs:=20=D1=81=D0=BE=D0=B3=D0=BB=D0=B0=D1=88?= =?UTF-8?q?=D0=B5=D0=BD=D0=B8=D1=8F=20=D1=88=D0=B0=D0=B1=D0=BB=D0=BE=D0=BD?= =?UTF-8?q?=D0=B0=20=D0=BA=D0=BE=D0=BC=D0=B0=D0=BD=D0=B4=D1=8B,=20=D1=80?= =?UTF-8?q?=D0=B0=D1=81=D0=BA=D0=BB=D0=B0=D0=B4=D0=BA=D0=B0=20api/src,=20?= =?UTF-8?q?=D1=81=D1=85=D0=B5=D0=BC=D0=B0=20dkb,=20=D0=BA=D0=BE=D0=BD?= =?UTF-8?q?=D1=82=D1=80=D0=B0=D0=BA=D1=82=20NATS=20v1.0?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 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 --- context/00-workspace-layout.md | 5 + context/03-architecture.md | 200 +++++++++++++++++++++++++++++---- 2 files changed, 182 insertions(+), 23 deletions(-) diff --git a/context/00-workspace-layout.md b/context/00-workspace-layout.md index c5640a8..567713b 100644 --- a/context/00-workspace-layout.md +++ b/context/00-workspace-layout.md @@ -34,6 +34,11 @@ `../CLAUDE.md` и `../docs/`. Шаблоны — `docs/repo-templates/`. - Один `claude`-сеанс может править несколько репозиториев, но PR — по одному на репозиторий; в описании PR указывать связанные PR в других репозиториях. +- **Worktree вместо веток в основном checkout.** Хуки запрещают запись в `main`; работа идёт в worktree. Для среза, + затрагивающего несколько репозиториев, worktree собираются в зеркальный workspace `.wt/<срез>/` рядом с основным: + `docs/`, `arm-core/`, `tabular-parser/`, `scan-gateway/` — worktree веток `feat/-`, а `fixtures/`, + `reference-old-arm/`, `CLAUDE.md`, `.claude/` — симлинки на основной workspace. Так относительные ссылки `../docs`, + `../fixtures` и хуки работают и в ветке. `.wt/` в git не попадает (workspace — не репозиторий). ## Шаблон команды: из нулевого коммита существующего репозитория diff --git a/context/03-architecture.md b/context/03-architecture.md index 6a2ce27..b277b30 100644 --- a/context/03-architecture.md +++ b/context/03-architecture.md @@ -38,10 +38,17 @@ ## Контракт обмена через NATS (DECISION-4) -| Поток | Subject | Кто пишет | Полезная нагрузка | -|---|---|---|---| -| запрос | `recognition.tabular.request` / `recognition.image.request` | arm-core (outbox из `recognition_job`) | `job_id`, `file_id`, `s3` (bucket/key исходника), `file_type`, `schema_version`, `reply_deadline` | -| результат | `recognition.result` | tabular-parser / scan-gateway | `job_id`, `status` (done/error), `error_code`, `service_name/version`, `result_s3` (bucket/key JSON), `result_schema_version`, `metrics`, `group_schema` (только tabular) | +Схемы — `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` | `:` | +| результат | `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::` | + +`group_schema` в конверт не входит — ядро берёт его из `documents[].group_schema` файла результата при маппинге. +Один stream `RECOGNITION` (subjects `recognition.>`); durable consumer'ы: `tabular-parser` и `scan-gateway` — запросы, +`arm-core-results` — результаты. Правила: - `recognition_job` создаётся в статусе `queued` **в одной транзакции** со `stored_file`; фоновый публикатор переводит в @@ -54,9 +61,11 @@ ## Схема ключей S3 -`{bucket_role}/{file_id}/{job_id}/{name}`; роли: `uploads` (исходники, срок — весь срок хранения), `results` +**Один бакет** (`OPENBAO__MINIO__MINIO_BUCKET` шаблона); роль объекта — первый сегмент ключа: +`{bucket_role}/{file_id}/{job_id}/{name}`. Роли: `uploads` (исходники, срок — весь срок хранения), `results` (нормализованные JSON; ретеншн — ARCH-010, открыт), `exports` (7 дней), `archive` (data lake). Все объекты регистрируются -в `s3_object` (sha256, size, expires_at). Сырой ответ OCR `raw/{job_id}.json` — по конвенции, без записи в БД. +в `s3_object` (`bucket_role`, `bucket`, `key`, sha256, size, expires_at). Сырой ответ OCR `raw/{job_id}.json` — по +конвенции, без записи в БД. Lifecycle-правила — по префиксам. ## Аутентификация и авторизация @@ -69,24 +78,30 @@ блокировке (`consolidation_lock`). Право говорит «может», policy — «эту ли и сейчас ли». 5. Локальная разработка: заглушка-эмитент JWT с теми же claims (feature-flag `AUTH_DEV_ISSUER`). -## Слои arm-core (предложение; подстроить под шаблон команды) +## Слои arm-core (по раскладке шаблона команды) -Ниже — структура репозитория `arm-core`; `tabular-parser` и `scan-gateway` — отдельные репозитории по тому же шаблону -без `frontend/`. +Структура — раскладка шаблона `api/src/` (см. «Соглашения шаблона» ниже); `tabular-parser` и `scan-gateway` — те же +каталоги без `web/`, `db/`, `models/`, `alembic/`. Соответствие прежним названиям этого документа: `backend/app/` → +`api/src/`, `integrations/` → `core/`, `migrations/` → `api/alembic/`, `frontend/` → `web/`. ``` -backend/app/ - api/ # роутеры FastAPI по контекстам: directories, files, documents, consolidation, kfo, ex, audit, users - domain/ # чистая предметная логика без I/O: osv parsing rules, period normalize, matching, elimination, - # checks, thresholds, status machine, policy - services/ # use-cases: оркестрация domain + repositories + outbox - repositories/ # SQLAlchemy, запросы-вычисления (сводная ОСВ, шахматка) — SQL + pandas там, где уместно - models/ # ORM 1:1 с DBML v5.13; имена таблиц/колонок — как в DBML - integrations/ # nats (outbox publisher, result consumer), s3, raisa - migrations/ # alembic -frontend/ +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/ -tests/ # фикстуры — из ../fixtures (ARM_KA_FIXTURES) ``` Принципы: `domain/` тестируется без БД на фикстурах из `../fixtures`; вычисляемые объекты консолидации возвращаются @@ -98,8 +113,147 @@ tests/ # фикстуры — из ../fixtures (ARM_KA_FIXTU (маршрут КФО) + `recognition_status` (жизненный цикл задания). Переходы — `05-domain-rules.md` §9. Реализовать как явные таблицы переходов в `domain/`, а не `if`-ы по роутерам. +## Соглашения шаблона команды + +Три сервисных репозитория развёрнуты из нулевого коммита репозитория команды `RSHB/DKB_ARMKA` (`ba172f8`, «Шаблон +FastAPI» платформы РАИСА/aurora): дерево импортировано как есть (`arm-core@ba92222`), доменный код удалён, каркас +оставлен (`8e1f423`). **Документы подстраиваются под шаблон, а не наоборот.** Ниже — что шаблон задаёт и что мы к нему +добавляем. Один и тот же текст действует для всех трёх репозиториев; отличия сервисов — в разделе «Что вырезано». + +### Раскладка репозитория + +``` +/ + 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/`, имя файла стандартное + `_.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+ (одна БД, схема `public`), NATS JetStream (один stream `RECOGNITION`, 3 subject), S3-совместимое -хранилище (4 бакета по ролям или один бакет с префиксами + lifecycle), Gitea + CI (lint, pytest, миграции с нуля, -сборка образов), реестр образов. Секреты — через переменные окружения. +PostgreSQL 15+ (одна БД, схема `dkb` — как в шаблоне), NATS JetStream (один stream `RECOGNITION`, 3 subject; +аутентификация nkeys/JWT, как в шаблоне), S3-совместимое хранилище (один бакет; префиксы-роли + lifecycle по префиксам), +Nexus (pip/npm, пакеты `raisa_*`), Gitea + CI (lint, pytest, миграции с нуля, сборка образов), реестр образов. +Секреты — через переменные окружения (`OPENBAO__*`).