arm-core/README.md
Andrey Epifancev 8bab965988 fix: dev-запуск слушает только 127.0.0.1 (M-11)
В api/.env.example задан SERVER__HOST=127.0.0.1 (и SERVER__PORT=8000):
при DEBUG и AUTH_DEV_ISSUER эндпоинт dev-токенов выдаёт токен любому, кто
знает email. Значение приложения по умолчанию (0.0.0.0) не меняется.
.env.test — те же ключи; новый тест сверяет набор ключей .env.example и
.env.test. README: рекомендация в быстром старте и строка таблицы.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
2026-10-04 01:55:59 +04:00

360 lines
27 KiB
Markdown
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

# 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
```
Сервер слушает `127.0.0.1:8000` (`SERVER__HOST`, `SERVER__PORT` из `api/.env`). Для dev-запуска оставляйте loopback:
при `DEBUG=true` и `AUTH_DEV_ISSUER=true` эндпоинт dev-токенов выдаёт токен любому, кто знает email пользователя, —
не открывайте его в сеть. Дальнейшие команды — из другого терминала.
### 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":"<JWT>","token_type":"bearer"}
curl -s localhost:8000/api/v1/me -H "Authorization: Bearer <JWT>"
```
```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`).
### Что вы увидите в логе при запуске
Старт занимает около секунды; трейсбэков в логе нет. SMB (горячие папки ИС Сканирование из шаблона) необязателен и по
умолчанию выключен: в `.env.example` `OPENBAO__SMB__SMB_HOST` пуст, сессия SMB не регистрируется (сообщение об этом —
только на уровне DEBUG). Если `SMB_HOST` задан, регистрация выполняется в отдельном потоке (цикл событий не
блокируется; старт ждёт её завершения не дольше тайм-аута `smbclient`, 60 с); ошибка подключения логируется трейсбэком
`Ошибка при регистрации сессии SMB`, приложение продолжает работу.
Строки `Бакет проверен`, `Подключение к NATS установлено` и `Подписка NATS активна. subject=recognition.result,
queue=arm-core` означают, что S3 и NATS из compose подключены (MinIO из compose слушает `http`, поэтому в `.env`
задано `OPENBAO__MINIO__MINIO_SECURE=false`).
Недоступность S3 или NATS при старте приложение не роняет и не задерживает (недоступный SMB, если он настроен, — не
роняет): ошибка логируется, состояние NATS видно в `/healthcheck`. К NATS ядро подключается в фоне и повторяет попытки
само (`nats=down`, пока связи нет; warning в логе — на первую ошибку и дальше не чаще раза в минуту); после разрыва
nats-py переподключается и восстанавливает подписку.
При остановке ресурсы закрываются по шагам: фоновые задачи обмена → NATS → S3 → SMB → пул соединений БД. Ошибка
одного шага логируется (`Ошибка при остановке приложения. step=...`) и не отменяет следующие.
Предупреждение «Включён 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`); в `.env.example` — `127.0.0.1` (dev-запуск только на loopback) |
| `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 не нужен). Пусто — сессия SMB не регистрируется; задан — регистрация при старте (в отдельном потоке) и сброс кэша соединений при остановке |
| `OPENBAO__SMB__SMB_PATH` | пусто | путь SMB (в Срезе 0 не используется) |
| `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_<hex>`, мигрируется `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.