При непустой `role_permission` вставляются пары, у которых роль или право вставлены этим же прогоном (`_upsert` возвращает вставленные ключи): их администратор снять не мог. Прочие пары по-прежнему не трогаются — снятые права не возвращаются. Убрана мёртвая ветка `unchanged` (R-9): при пустой таблице совпадений быть не может, при заполненной сверка не нужна. Тесты: новое право и новая роль во временном seed-каталоге → повторный seed выдаёт только их пары, снятая связь не возвращается; администратор снял все связи → матрица заливается заново. README, CLAUDE.md, комментарий `role_permissions.yaml` и докстринг `seed.py` уточнены. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_012vJkmBGKKqMoEMzvsXG3y2
96 lines
12 KiB
Markdown
96 lines
12 KiB
Markdown
# CLAUDE.md — arm-core (ядро АРМ КА)
|
||
|
||
Общий контекст системы — `../CLAUDE.md` и `../docs/` (workspace). Этот файл — только про этот репозиторий.
|
||
|
||
## Что это
|
||
Ядро: справочники, файлы/документы, маппинг результатов распознавания, вычисления консолидации, маршрут согласования,
|
||
ЕХ, аудит, права. **Единственный писатель в PostgreSQL.** Монорепо `api/` (FastAPI) + `web/` (React/TS) по шаблону
|
||
команды «Шаблон FastAPI» (нулевой коммит `RSHB/DKB_ARMKA@ba172f8`; импорт — `ba92222`, очистка от домена — `8e1f423`).
|
||
|
||
## Соглашения шаблона
|
||
Полный текст — `../docs/context/03-architecture.md`, раздел «Соглашения шаблона команды». Главное для этого репозитория:
|
||
- **Команды** — `make` из корня репозитория (Makefile в корне, backend-цели внутри делают `cd api`, venv — `api/.venv`):
|
||
`venv` (uv, Python 3.11, без `raisa_*`), `lint` (ruff + mypy), `test` (без БД), `test-integration` (PostgreSQL/NATS
|
||
из compose, включает цикл миграций), `check` (= lint + test, Docker не нужен), `up` (compose из `../docs/dev/`),
|
||
`migrate`, `seed` / `seed-dev` (справочники / + dev-пользователи), `dbml-check` (сверка ORM↔DBML), `migration-cycle`
|
||
(только цикл миграций), `check-infra`, `docker-build`, `web-check` (biome + tsc + build), `check-full` (check +
|
||
dbml-check + check-infra + test-integration + docker-build + web-check; **перед PR**, вывод — в описание PR).
|
||
Вручную — из `api/`: `python run_server.py`, `alembic upgrade head`, `pytest`; из `web/`: `npm run start|build`.
|
||
Бутстрап в банке — `setup/setup.sh` (pip/npm через Nexus).
|
||
- **Раскладка `api/src/`**: `src/api/endpoints` → `services` → `repositories` → `models`; DTO — `schemas/`; инфраструктура —
|
||
`core/` (config, clients, nats, logger, exception_handlers, auth); `db/` — engine и сессия. Наши добавления:
|
||
`domain/` (чистые функции по §§ правил, без БД и сессий: `policy.py` — видимость справочников §10.1,
|
||
`directory_import.py` — чтение xlsx/csv и план импорта §13, `aliases.py` — нормализация алиаса §3, `inn.py` — формат
|
||
ИНН; для вычислений консолидации — Срез 3+), `contract/` (модели контракта обмена), `api/scripts/` (`check_dbml_vs_orm.py`,
|
||
`seed.py` + `seed_lib/`, `check_infra.py`), данные seed — `api/seed/`.
|
||
- **DI** — ручные фабрики `Depends` в `src/api/dependencies/`; `storage`/`nats` — из `request.app.state`. **Сессия** —
|
||
только `session: SessionDep` (`src/api/dependencies/db.py` = `Depends(get_async_session, scope="function")`): одна
|
||
транзакция на запрос (`get_async_session` открывает `session.begin()`), commit **до** отправки ответа, rollback при
|
||
любом исключении; объявление через голый `Depends(get_async_session)` даёт вторую сессию и ловится тестом
|
||
`test_api_routes_declare_session_with_function_scope`. Сервисы не коммитят сами.
|
||
- **Настройки** — pydantic-settings, `api/.env`, вложенность `__`, инфраструктура под `OPENBAO__*`. Флаги разработки
|
||
(`AUTH_DEV_ISSUER`, `DEBUG`) и обмена (`NATS_ENABLED`, по умолчанию выключен) — отдельные поля `Settings`. NATS
|
||
управляется только `NATS_ENABLED` (от `DEBUG` не зависит); при `DEBUG=false` шаблонный импорт
|
||
`raisa_fastapi_protected_api` в `app.py` условный (пакета нет вне банка — не ошибка); middleware РАИСА в Срезе 0
|
||
не подключается.
|
||
- **БД** — схема **`dkb`** (MetaData, `search_path`, Alembic); PK `uuid` через `UUIDPrimaryMixin`; ревизии в
|
||
`api/alembic/versions/`; автогенерация — черновик, финал сверяется с DBML скриптом.
|
||
- **Ошибки** — `common/exceptions.py` → `core/exception_handlers.py`. Доменные ошибки (`DomainError`: `NotFoundError` 404
|
||
`NOT_FOUND`, `ConflictError` 409, `DomainValidationError` 422; код и доп. поля задаёт вызывающий) отдаются как
|
||
`{"detail": {"code", "message", **extra}}` (message по-русски, `code`/`message` в `extra` запрещены); `HTTPException`
|
||
(401/403/413) и ошибки БД — `{"detail": "<строка>"}`, 422 валидации тела — массив. Объект вне видимости пользователя —
|
||
404 с тем же телом, что у несуществующего (не 403). Каталог кодов — README, «Ошибки API». **Логи** — loguru.
|
||
- **Тесты** — pytest-asyncio (auto, session loop), `APP_ENV=test`, `ENV_FILE=.env.test`; маркеры `integration` (нужны
|
||
PostgreSQL/NATS) и `fixtures` (реальные данные из `../fixtures`); БД сессии — свежая `armcore_pytest_<hex>` с миграциями,
|
||
удаляется в конце; `domain/` — без БД.
|
||
- **Линтеры** — backend `ruff` + `mypy` (`api/pyproject.toml`); frontend `biome` (ширина 140, одинарные кавычки) + `tsc`.
|
||
- **Frontend** — React 18 + TS strict, rsbuild, antd 6 (основной UI-кит), react-router 7; `layouts/` — страницы
|
||
(`Main`, `Admin`), `widgets/` — блоки (`Sidebar`, `Admin/*`), `components/` — примитивы; env только `REACT_APP_*` через
|
||
`conf/config.ts`; `REACT_APP_ROOT_PATH` = `ROOT_PATH` бэка. Redux Toolkit из шаблона подключён (`store/store.ts`,
|
||
`Provider` в `App.tsx`), но стор пуст и `store/slices/` без слайсов: состояние экранов `/admin` — локальные хуки
|
||
(`widgets/Admin/hooks.ts`), профиль — контекст. `api/client.ts` — единый клиент (`apiFetch`, `api.get|post|put|patch`,
|
||
префикс `${Config.API_URL}/api/v1`, Bearer-токен, `ApiError {status, code, message}` по трём формам `detail`, событие
|
||
`armka:unauthorized` на 401). `auth/` — `AuthGate` (профиль `/me` → `MeContext`/`useCurrentUser`), `DevLogin` (только не
|
||
в production-сборке: `POST /dev/token`), `token.ts` (sessionStorage с запасом в памяти), `permissions.ts` (коды прав
|
||
для скрытия пунктов меню; доступ решает сервер). `/admin` (`layouts/Admin.tsx`, право `USERS_ADMIN`) — пользователи,
|
||
роли, матрица прав.
|
||
- **Сборка** — платформа (Python 3.11) запускает `api/run_server.py` (`SERVER__HOST/PORT`), ресурсы — `raisa_config.yml`;
|
||
SPA: `web/dist` → `api/web/`. `Dockerfile` (`python:3.11-slim`) в корне — для dev-compose и `make check-full`
|
||
(`docker build`; убран из `.gitignore` шаблона). CI в репозитории нет: перед PR — `make check-full`. Без Nexus:
|
||
`make venv` ставит зависимости без `raisa_*`, npm-пакеты `raisa-*` — `optionalDependencies`.
|
||
- **NATS без JetStream** (`NATS_ENABLED=true`): `core/nats.py` — подключение в фоне (supervisor повторяет попытки),
|
||
подписка queue group `arm-core` на `recognition.result`. Обмен — `services/recognition_exchange.py`: `publish_pending`
|
||
(outbox), `expire_deadlines` (вахтёр сроков), `handle_message` (inbox: ack — конверт `processing`, результат);
|
||
публикатор и вахтёр — фоновые задачи lifespan. Запрос без ack повторно публикуется (`ACK_TIMEOUT_SECONDS`=30 ×
|
||
`OUTBOX_MAX_REPUBLISH`=3, счётчик — `recognition_job.metrics.republish_count`) → `dlq`; дедуп — `request_msg_id` /
|
||
`result_msg_id`. Статусы задания в Срезе 0 `audit_event` не пишут.
|
||
- **Аудит** — `services/audit.py`, единственная точка записи `audit_event` (append-only: только `record`, в текущей транзакции;
|
||
тест `test_audit_service.py` сканирует `src/` на UPDATE/DELETE по `AuditEvent`). В маршрутах — `RequestAudit`
|
||
(`get_request_audit`: пользователь, IP, user-agent).
|
||
- **Справочники и права (Срез 1)** — видимость только через `domain/policy.py` (`VisibilityScope`: ГО — всё, филиал — свои
|
||
ГК/ЮЛ/ГСК/ГСЗ и монозаёмщики), код роли в логике не читается; `domain/inn.py` — ИНН строкой (ЮЛ 10, ИП 12 цифр,
|
||
без контрольных разрядов, OQ-88); `domain/aliases.py` — `normalized_alias` по §3; `domain/directory_import.py` — импорт
|
||
ЕЦП (§13, зависимость `openpyxl` для xlsx; допущения `ASSUMPTION(OQ-87…91)`). Администрирование — `services/users_admin.py`
|
||
(`LAST_ADMIN` с блокировкой строки права). Порядок: маршрут (`require(code)`) → policy → сервис → аудит.
|
||
- **Аутентификация** — `core/auth.py` (проверка JWT), `src/api/dependencies/auth.py` (`current_user`, `require(code)`);
|
||
dev-эмитент (`POST /api/v1/dev/token`) существует только при `AUTH_DEV_ISSUER=true` **и** `DEBUG=true`.
|
||
Аудит входа — `services/session_audit.py` (`app.state.session_audit`): `LOGIN` (dev-токен; первый запрос после старта),
|
||
`LOGIN_FAILED` (`AuthenticationFailedError` 401/403 → обработчик пишет в отдельной транзакции `session_transaction`).
|
||
- **Seed** — `python -m scripts.seed [--dev]` (`make seed|seed-dev`), идемпотентен (матрица `role_permission` — целиком только в
|
||
пустую таблицу, в заполненную — лишь пары впервые созданных ролей/прав); `--dev` — только для разработки (CLI отказывает с кодом 2 при `DEBUG=false`).
|
||
**Сверка ORM↔DBML** — `scripts/check_dbml_vs_orm.py` (`make dbml-check`).
|
||
|
||
## Зависимости от соседей
|
||
- Контракты обмена: `../docs/contracts/recognition-request.schema.json`, `recognition-result-message.schema.json`,
|
||
`recognition-result.schema.json`; в репозитории — байт-идентичные копии в `api/contracts/` (сверяются тестами,
|
||
`tests/test_contract.py`). `ex-payload-v1.schema.json` (payload ЕХ) появится в Срезе 7 (`../docs/context/09-delivery-plan.md`).
|
||
- Фикстуры: `../fixtures/` (env `ARM_KA_FIXTURES`); при отсутствии тесты с маркером `fixtures` пропускаются с явной причиной (skip reason).
|
||
- NATS subjects и схема ключей S3 — `../docs/context/03-architecture.md`.
|
||
|
||
## Правила репозитория
|
||
- ORM 1:1 с `../docs/model/data-model-v5.14.dbml` (текущая версия; миграции `0001_dbml_v5_13` + `0002_dbml_v5_14`); `api/scripts/check_dbml_vs_orm.py` (`make dbml-check`, входит в `check-full`) — обязательная проверка.
|
||
- Алгоритмы — только из `../docs/context/05-domain-rules.md`; номер § — в docstring.
|
||
- Вычисляемые объекты консолидации — `api/src/domain/`, без ORM-таблиц.
|
||
- Фронт: пользователь слаб во фронте — для TS/React изменений делай ревью сам и объясняй решения в PR.
|
||
- OpenSpec: `openspec/` здесь; сквозные изменения — change в `../docs`.
|