# arm-core — ядро АРМ КА Ядро автоматизированного рабочего места кредитного аналитика Департамента крупного бизнеса (АРМ КА, АО «Россельхозбанк»): справочники, файлы и документы, приём результатов распознавания, вычисления консолидации (КФО ГСК), маршрут согласования, Единое хранилище (ЕХ), аудит и права. **Единственный писатель в PostgreSQL**: сервисы распознавания (`tabular-parser`, `scan-gateway`) видят только NATS и S3. Монорепозиторий по шаблону команды «Шаблон FastAPI»: `api/` — бэкенд (FastAPI, SQLAlchemy 2 async, Alembic, PostgreSQL 16, core NATS без JetStream), `web/` — фронтенд (React + TypeScript, rsbuild). Этот репозиторий — часть workspace АРМ КА. Общий контекст, модель данных, правила предметной области и контракты — в соседних репозиториях: - [`../CLAUDE.md`](../CLAUDE.md) — инварианты и процесс работы всего workspace; - [`../docs/`](../docs/) — контекст (`context/`), модель данных (`model/`), контракты (`contracts/`), окружение (`dev/`); - [`CLAUDE.md`](CLAUDE.md) — соглашения этого репозитория. Срез 0 («скелет ядра»): схема БД по DBML v5.13, справочники (seed), аутентификация и права, `GET /api/v1/me`, `/healthcheck`. Спецификация — `openspec/changes/slice-0-skeleton/`. ## Предварительные требования - **Python 3.11** (версия платформы) и **[`uv`](https://docs.astral.sh/uv/)** — `make venv` создаёт окружение и ставит зависимости через `uv`; без него цель не работает. Системный `python3` другой версии не подойдёт: `uv` сам подберёт интерпретатор 3.11. - **Docker** и compose v2 — для PostgreSQL, NATS и MinIO (compose лежит в `../docs/dev/`). - **Node.js** и `npm` — только для `web/`. Для запуска бэкенда не нужны. - `make`, `curl`. Репозитории workspace должны лежать рядом: `arm-core/` и `docs/` — соседние каталоги (так же устроено зеркало `.wt/<срез>/`). Пути `../docs/...` ниже и в `Makefile` рассчитаны на это. ## Быстрый старт Путь «с нуля до зелёного `/api/v1/me`». Команды `make` выполняются из корня репозитория. ### 1. Виртуальное окружение ```bash make venv ``` Создаёт `api/.venv` (Python 3.11) и ставит `api/requirements.txt` и `api/requirements-dev.txt`. Пакеты `raisa_*` из Nexus банка вне банка недоступны, поэтому цель пропускает строки `raisa_*` (подробнее — «Работа без Nexus»). Повторный запуск безопасен. ### 2. Инфраструктура (PostgreSQL, NATS, MinIO) Файл `.env` compose — отдельный от `api/.env`; он создаётся один раз и в git не попадает: ```bash cp ../docs/dev/.env.example ../docs/dev/.env make up # docker compose -f ../docs/dev/docker-compose.yml up -d ``` `make up` поднимает `postgres` (5432), `nats` (4222, мониторинг 8222), `minio` (9000, консоль 9001) и одноразовый `minio-init`, который создаёт бакет `arm-ka` и завершается с кодом 0. Команда возвращается сразу, а PostgreSQL становится готовым примерно через 6 секунд: **дождитесь `healthy`** у трёх долгоживущих сервисов, прежде чем выполнять `make migrate`: ```bash docker compose -f ../docs/dev/docker-compose.yml up -d --wait postgres nats minio # возвращается, когда все healthy docker compose -f ../docs/dev/docker-compose.yml ps # проверка статуса ``` (голый `--wait` без списка сервисов возвращает код 1 из-за завершающегося `minio-init`; поэтому `make up` для бакета и `--wait` со списком — вместе). Подробности, смена портов, сброс — [`../docs/dev/README.md`](../docs/dev/README.md). ### 3. Настройки приложения ```bash cp api/.env.example api/.env ``` Значения из `api/.env.example` уже совпадают с `../docs/dev/.env.example` (стандартные порты и учётные данные compose), менять ничего не нужно. Менять `api/.env` придётся, только если вы изменили порты или пароли в `../docs/dev/.env` (правьте обе части). `api/.env` в git не попадает. ### 4. Миграции и справочники ```bash make migrate # alembic upgrade head: схема dkb, 27 enum-типов, 43 таблицы make seed-dev # справочники продукта + dev-пользователи (по одному на каждую роль) ``` `make seed` загружает только справочники (роли, права, филиалы, план счетов, правила элиминации, чек-лист), без dev-пользователей. Seed идемпотентен: повторный запуск обновляет данные и ничего не дублирует. ### 5. Запуск ```bash cd api && .venv/bin/python run_server.py ``` Сервер слушает `0.0.0.0:8000` (`SERVER__HOST`, `SERVER__PORT`). Дальнейшие команды — из другого терминала. ### 6. Проверка ```bash curl -s localhost:8000/healthcheck ``` ```json {"uptime":"2026-10-03T18:37:15.121394+00:00","version":"1.0.0","nats":"ok"} ``` `nats`: `ok` — подключён, `down` — включён (`NATS_ENABLED=true`), но связи нет, `off` — выключен. Dev-токен по email любого dev-пользователя и профиль текущего пользователя: ```bash curl -s -X POST localhost:8000/api/v1/dev/token \ -H 'Content-Type: application/json' \ -d '{"email":"ka_rf@armka.local"}' # {"access_token":"","token_type":"bearer"} curl -s localhost:8000/api/v1/me -H "Authorization: Bearer " ``` ```json { "id": "b8d7595a-787f-4bcb-8e93-7612cc8afba3", "email": "ka_rf@armka.local", "full_name": "Dev: Аналитик РФ", "regional_office": { "id": "3b38e823-e09a-47ce-a296-061948a200b0", "name": "Ростовский региональный филиал", "is_head_office": false }, "business_segment": "DKB", "roles": ["KA_RF"], "permissions": [ "CONSOLIDATION_EDIT", "CONSOLIDATION_VIEW", "DIRECTORY_EDIT", "DIRECTORY_VIEW", "DOCUMENT_EDIT", "DOCUMENT_VIEW", "EXPORT", "EX_VIEW", "KFO_EXPORT", "KFO_VIEW" ] } ``` (идентификаторы в вашей БД будут другими). Токен действует 8 часов. Интерактивная документация API — `/docs` (Swagger UI), схема — `/openapi.json`. Dev-пользователи (`api/seed/dev/users.yaml`, создаются `make seed-dev`): | Email | Роль | Филиал | |---|---|---| | `ka_rf@armka.local` | `KA_RF` | Ростовский региональный филиал | | `rka@armka.local` | `RKA` | Ростовский региональный филиал | | `client_manager@armka.local` | `CLIENT_MANAGER` | Краснодарский региональный филиал | | `ka_go@armka.local` | `KA_GO` | Головной офис | | `curator_go@armka.local` | `CURATOR_GO` | Головной офис | | `expert@armka.local` | `EXPERT` | Головной офис | | `admin@armka.local` | `ADMIN` | Головной офис | Права не зашиты в код роли: `effective_permissions(user)` — объединение прав ролей пользователя (таблицы `permission`, `role_permission`). ### Что вы увидите в логе при запуске Старт занимает около секунды; в логе один трейсбэк — это ожидаемо в dev, приложение при этом работает: - Трейсбэк SMB: `Ошибка при регистрации сессии SMB ... Failed to connect to 'localhost:445': Connection refused`. Настройки `OPENBAO__SMB__*` — заглушки, SMB в Срезе 0 не используется; игнорируйте. Пока на `localhost:445` ничего не слушает, отказ приходит сразу (старт не задерживается); если порт отвечает, но молчит, ожидание ограничено тайм-аутом `smbclient` (60 с). Строки `Бакет проверен`, `Подключение к NATS установлено` и `Подписка NATS активна. subject=recognition.result, queue=arm-core` означают, что S3 и NATS из compose подключены (MinIO из compose слушает `http`, поэтому в `.env` задано `OPENBAO__MINIO__MINIO_SECURE=false`). Недоступность SMB, S3 или NATS при старте приложение не роняет и не задерживает: ошибка логируется, состояние NATS видно в `/healthcheck`. К NATS ядро подключается в фоне и повторяет попытки само (`nats=down`, пока связи нет; warning в логе — на первую ошибку и дальше не чаще раза в минуту); после разрыва nats-py переподключается и восстанавливает подписку. Предупреждение «Включён dev-эмитент токенов … только для разработки» появляется не при старте, а при первом запросе к защищённому эндпоинту (`/api/v1/me`): проверяющий токены создаётся лениво (штатно, см. «Безопасность dev-режима»). ### Остановка и сброс ```bash docker compose -f ../docs/dev/docker-compose.yml down # остановить, данные (тома) сохраняются docker compose -f ../docs/dev/docker-compose.yml down -v # полный сброс: БД и бакет с нуля (затем снова migrate и seed-dev) ``` Имя проекта compose фиксировано (`arm-ka`), тома общие для всех worktree. ## Конфигурация Настройки — pydantic-settings; читаются из переменных окружения и файла `api/.env`. Вложенные настройки разделяются `__`. Файл окружения выбирается переменной `ENV_FILE` (по умолчанию `.env`, путь относительно текущего каталога — `api/`; значение читается при импорте). Тесты используют `api/.env.test` (`pytest.ini`). Полный пример — `api/.env.example`. | Переменная | По умолчанию | Назначение | |---|---|---| | `ENV_FILE` | `.env` | файл окружения (относительно `api/`) | | `DEBUG` | `false` | режим разработки. `true` необходим dev-эмитенту токенов; при `false` пакеты РАИСА подгружаются условно | | `SERVER__HOST`, `SERVER__PORT` | `0.0.0.0`, `8000` | адрес и порт сервера (`run_server.py`) | | `ROOT_PATH` | `/` | корневой путь за прокси (должен совпадать с `REACT_APP_ROOT_PATH` фронта) | | `VERSION` | `1.0.0` | версия в `/healthcheck` и OpenAPI | | `OPENBAO__DATABASE__DATABASE_URL` | — (обязательно) | PostgreSQL, драйвер asyncpg: `postgresql+asyncpg://armka:armka@localhost:5432/armka` | | `OPENBAO__MINIO__MINIO_URL` | — (обязательно) | адрес MinIO без схемы (`host:port`, так ждёт `miniopy-async`) | | `OPENBAO__MINIO__MINIO_ADMIN_LOGIN` | — (обязательно) | логин MinIO | | `OPENBAO__MINIO__MINIO_ADMIN_PASSWORD` | — (обязательно) | пароль MinIO | | `OPENBAO__MINIO__MINIO_BUCKET` | — (обязательно) | бакет MinIO (`arm-ka`) | | `OPENBAO__MINIO__MINIO_SECURE` | `true` | `true` — MinIO по `https` (платформа), `false` — по `http` (MinIO из compose; так в `.env.example`) | | `OPENBAO__NATS__NATS_URL` | — (обязательно) | адрес NATS (`nats://localhost:4222`) | | `OPENBAO__NATS__NATS_SEED` | пусто | nkey seed; пусто — подключение без учётных данных (compose, тесты). Задаётся вместе с `NATS_JWT` или не задаётся вовсе | | `OPENBAO__NATS__NATS_JWT` | пусто | JWT NATS; см. `NATS_SEED` | | `OPENBAO__SMB__SMB_HOST` | — (обязательно) | заглушка SMB (в Срезе 0 не используется, блок обязателен) | | `OPENBAO__SMB__SMB_PATH` | — (обязательно) | заглушка SMB | | `OPENBAO__SMB__SMB_LOGIN` | — (обязательно) | заглушка SMB | | `OPENBAO__SMB__SMB_PASSWORD` | — (обязательно) | заглушка SMB | | `NATS_ENABLED` | `false` | подключение к NATS и обмен через `recognition_job`: фоновые outbox-публикатор и вахтёр сроков ответа (период 30 с, `processing`/`published` с прошедшим `reply_deadline` → `timeout`); без него фоновых задач нет, `/healthcheck` показывает `nats=off` | | `OUTBOX_POLL_SECONDS` | `5` | период опроса outbox (`queued` → публикация → `published`) | | `ACK_TIMEOUT_SECONDS` | `30` | ожидание ack сервиса распознавания до повторной публикации | | `OUTBOX_MAX_REPUBLISH` | `3` | число повторных публикаций до перевода в `dlq` | | `AUTH_DEV_ISSUER` | `false` | dev-эмитент токенов: работает **только** при `AUTH_DEV_ISSUER=true` **и** `DEBUG=true` | | `AUTH_DEV_SECRET` | пусто | секрет HS256 dev-эмитента (не короче 32 байт); только для разработки | | `AUTH_JWKS_URL` или `AUTH_PUBLIC_KEY` | пусто | проверка токенов РАИСА: JWKS по URL либо публичный ключ (PEM) | | `AUTH_ISSUER`, `AUTH_AUDIENCE` | пусто | проверка `iss` и `aud` токена РАИСА | | `AUTH_EMAIL_CLAIM` | `email` | claim токена с email пользователя (сопоставляется с `app_user.email`) | Параметры JWT РАИСА (алгоритм, claim, эмитент) пока неизвестны — это открытый вопрос ([`../docs/context/07-open-questions.md`](../docs/context/07-open-questions.md)); `AUTH_JWKS_URL`/`AUTH_PUBLIC_KEY` и связанные поля — каркас под него. Если не настроено ни dev-эмитента, ни ключей РАИСА, токены не принимаются вовсе (`/me` отвечает 401). Порты по умолчанию: приложение 8000; compose — PostgreSQL 5432, NATS 4222 (мониторинг 8222), MinIO 9000 (консоль 9001). Порты compose меняются в `../docs/dev/.env`, тогда же — соответствующие `OPENBAO__*` в `api/.env`. ## Команды `make` Все цели выполняются из корня репозитория (сами переходят в `api/`). | Цель | Что делает | |---|---| | `make venv` | `api/.venv` на Python 3.11 через `uv`, зависимости без `raisa_*` | | `make up` | поднимает compose из `../docs/dev` (PostgreSQL, NATS, MinIO, бакет) | | `make migrate` | `alembic upgrade head` на БД из `api/.env` | | `make seed` | загрузка справочников продукта (идемпотентна) | | `make seed-dev` | то же и dev-пользователи/филиалы (`--dev`) — только для разработки | | `make lint` | `ruff check`, `ruff format --check`, `mypy src scripts` | | `make test` | юнит-тесты: `pytest -m "not integration"`, Docker не нужен | | `make test-integration` | интеграционные тесты: `pytest -m integration`, нужны PostgreSQL (и NATS) | | `make dbml-check` | сверка ORM с `../docs/model/data-model-v5.13.dbml` | | `make migration-cycle` | миграции на временной БД: `upgrade head` → `downgrade base` → `upgrade head`, пустой autogenerate | | `make check` | быстрая проверка без Docker: `lint` + `test` | | `make docker-build` | сборка образа: `docker build -t arm-core .` | | `make web-check` | фронт: `npm install`, `biome check src`, `tsc --noEmit`, `npm run build` | | `make check-infra` | быстрая проверка, что PostgreSQL и NATS из compose доступны (иначе: `make up`) | | `make check-full` | полная проверка перед PR: `check` + `dbml-check` + `check-infra` + `test-integration` (в нём цикл миграций) + `docker-build` + `web-check`; нужна поднятая инфраструктура (`make up`) и Docker | ### Тесты - **Юнит-тесты** (`make test`) не обращаются ни к БД, ни к NATS, Docker не нужен. - **Интеграционные** (`make test-integration`) требуют PostgreSQL, тесты обмена через NATS — ещё и NATS. Рабочую БД `armka` они не трогают: на каждую сессию pytest создаётся свежая БД `armcore_pytest_`, мигрируется `alembic` и удаляется в конце. Параметры подключения берутся из `api/.env.test` (`pytest.ini` задаёт `ENV_FILE=.env.test`). Файл указывает на стандартные порты и учётные данные compose из `../docs/dev` (PostgreSQL `5432`, NATS `4222`, MinIO `9000`), поэтому после `make up` тесты работают «из коробки». То же относится к `make migration-cycle` и `make check-full`: они тоже читают `.env.test` через `pytest.ini`. - **Реальные данные** — репозиторий `../fixtures/` (путь можно задать переменной `ARM_KA_FIXTURES`). Тесты с маркером `fixtures` без каталога пропускаются с понятной причиной. ### Проверка ORM и DBML Модель данных — `../docs/model/data-model-v5.13.dbml`. ORM обязана совпадать с ней один в один: ```bash make dbml-check # пустой вывод и код 0 — совпадение; расхождения печатаются построчно ``` Другой путь к DBML: `make dbml-check DBML=<путь относительно api/>`. ### Миграции ```bash make migrate # применить: alembic upgrade head cd api && .venv/bin/alembic downgrade base # откатить до пустой БД make migration-cycle # проверить цикл upgrade -> downgrade -> upgrade на временной БД ``` Ревизии лежат в `api/alembic/versions/`; автогенерация — только черновик, финал сверяется с DBML (`make dbml-check`). ## Раскладка репозитория ``` api/ run_server.py точка входа (uvicorn; хост и порт — из настроек) alembic/ миграции (versions/0001_dbml_v5_13.py — вся модель v5.13) seed/core/ справочники продукта (роли, права, филиалы, план счетов, правила элиминации, чек-лист) seed/dev/ dev-данные: филиалы и пользователи по ролям scripts/ check_dbml_vs_orm.py (ORM <-> DBML), seed.py и seed_lib/ (загрузчик справочников) src/ app.py FastAPI-приложение: lifespan (S3, NATS, SMB), /healthcheck, раздача SPA api/endpoints/ эндпоинты (me.py, dev_auth.py); router.py собирает /api/v1 api/dependencies/ зависимости FastAPI (current_user, проверка прав) core/ config, auth (JWT), clients (S3), nats (подключение, publish, подписка), logger, exception_handlers db/ движок и сессия (схема dkb) models/ ORM-модели, 1:1 с DBML repositories/ доступ к данным schemas/ pydantic-схемы запросов и ответов services/ бизнес-логика common/ исключения domain/ (появится) чистые функции по §§ правил предметной области, без ORM tests/ pytest (юнит и integration/fixtures по маркерам) web/ фронтенд (React 18 + TypeScript, rsbuild, antd); сборка web/dist -> api/web/ setup/ бутстрап шаблона через Nexus банка (pip.conf, setup.sh) openspec/ спецификации и изменения (OpenSpec) docs/superpowers/ планы реализации raisa_config.yml ресурсы для платформы Dockerfile только для локального compose ``` Слои: `src/api/endpoints` → `services` → `repositories` → `models`. ## Работа без Nexus Пакеты `raisa_*` (Python) и `raisa-*` (npm) лежат в Nexus банка и вне банка недоступны. Что с этим сделано: - `make venv` ставит `requirements.txt` без строк `raisa_*` (фильтр `grep -v '^raisa_'`) и поэтому требует `uv`, а не `setup/setup.sh` (тот рассчитан на `pip.conf` с Nexus). - Код импортирует `raisa_*` условно: пакет middleware РАИСА импортируется только при `DEBUG=false`, и `ImportError` допустим — приложение работает без него. Аутентификация Среза 0 — собственная проверка JWT (`api/src/core/auth.py`). - В `web/package.json` пакеты `raisa-*` вынесены в `optionalDependencies`: `npm install` не падает, если Nexus недоступен. Файл `.npmrc` с адресом реестра (см. `setup/setup.sh`) вне банка не нужен. ## Безопасность dev-режима - `POST /api/v1/dev/token` выдаёт токен **любому**, кто знает email существующего активного пользователя: пароля нет. Только для локальной разработки и тестовых стендов. - Маршрут существует лишь при `AUTH_DEV_ISSUER=true` **и** `DEBUG=true`; при любом другом сочетании его нет вовсе (404), а dev-проверяющий токенов не создаётся даже при заданном `AUTH_DEV_SECRET`. В проде `DEBUG=false`, поэтому dev-эмитент там невозможен. - `AUTH_DEV_SECRET` из `api/.env.example` — публичный пример, в проде не использовать. `make seed-dev` в прод не запускать. - Порты compose по умолчанию публикуются только на `127.0.0.1` (`BIND_ADDR`); пароли `armka`/`minioadmin` — только dev. ## Соседние документы - [`CLAUDE.md`](CLAUDE.md) — соглашения этого репозитория; - [`../CLAUDE.md`](../CLAUDE.md) — workspace: инварианты, источники истины, процесс; - [`../docs/dev/README.md`](../docs/dev/README.md) — окружение разработки (compose), порты, типичные проблемы; - [`../docs/context/03-architecture.md`](../docs/context/03-architecture.md) — архитектура, соглашения шаблона, NATS и S3; - [`../docs/context/04-data-model-guide.md`](../docs/context/04-data-model-guide.md) и [`../docs/model/data-model-v5.13.dbml`](../docs/model/data-model-v5.13.dbml) — модель данных; - [`../docs/context/05-domain-rules.md`](../docs/context/05-domain-rules.md) — алгоритмы предметной области; - [`../docs/context/07-open-questions.md`](../docs/context/07-open-questions.md) — открытые вопросы; - [`../docs/context/09-delivery-plan.md`](../docs/context/09-delivery-plan.md) — план срезов; - [`../docs/contracts/`](../docs/contracts/) — JSON-схемы обмена с сервисами распознавания; - `openspec/changes/slice-0-skeleton/` — спецификация и решения Среза 0.