arm-core/CLAUDE.md
Andrey Epifancev ab606dc59a feat(model): DBML v5.15 — osv_data_type NU, email в нижнем регистре, миграция 0003
- OsvDataType.NU после BU, DBML_VERSION = 5.15, Makefile/dbml_parity/docstring'и на data-model-v5.15.dbml;
- миграция 0003_dbml_v5_15: ADD VALUE 'NU' AFTER 'BU' + lower(app_user.email); downgrade пересоздаёт osv_data_type
  (регистр email не возвращается);
- email хранится в нижнем регистре: users_admin.create_user и seed (_apply_users); с UNIQUE по строчному email гонка
  создания в другом регистре теперь ловится индексом (тест test_api_constraint_races обновлён);
- тесты: enum NU, откат на шаг, lower email на миграции, seed/админка, вставка NU, фиктивная v5.16.

Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_012vJkmBGKKqMoEMzvsXG3y2
2026-10-05 21:28:04 +04:00

12 KiB
Raw Blame History

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); нет заголовка Authorization или схема не Bearer, 403 «недостаточно прав» — не события входа.
  • 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.15.dbml (текущая версия; миграции 0001_dbml_v5_13 + 0002_dbml_v5_14 + 0003_dbml_v5_15); 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.