arm-core/CLAUDE.md
Andrey Epifancev 2c83005564 fix: seed выдаёт права новых ролей и прав в заполненную матрицу (ревью R-3, R-9, #9)
При непустой `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
2026-10-05 13:47:42 +04:00

96 lines
12 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.

# 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`.