arm-core/CLAUDE.md
Andrey Epifancev b5520696ef chore: DBML v5.22 — версия ORM и ссылки (ревью #161, AURORA-1446)
v5.22 структуру БД не меняет (comment.object_type: убраны chess_cell и
kfo_result_line из описания), миграция не нужна.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-23 17:59:35 +04:00

31 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 (только цикл миграций), e2e (сквозной прогон загрузки ОСВ с реальным tabular-parser, без профиля services — skip), check-infra, docker-build, web-check (biome + tsc + build), check-full (check + dbml-check + check-infra + test-integration + e2e + 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 — формат ИНН, file_types.py — тип файла по расширению и сигнатуре, osv_mapping.py — результат парсера → документ/строки ОСВ (§1.3, §1.5, §9.3); Срез 4 — consolidation_periods.py (выбор источников и приведение периодов, §2, Q1-эквивалентность ОСВ, R-14), consolidated_osv.py (чистая сборка свода: склейка кварталов, пробелы, дорезолв, фильтры, §2–§3), checklist.py (чек-лист комплекта, ячейки CellStatus); Срез 3b — statement_mapping.py (формы ББ/ОФР: период, черновик, статус D-6), statement_checks.py (остатки §7 в ядре: межформенная 1370↔2400 и subline_parents; соотношения §7 считает arm_statements), checklist_csv.py (CSV чек-листа); policy.py дополнен IN_WORK_STATUSES/ensure_version_editable, §10; для шахматки и КФО — Срезы 5–6), 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 по умолчанию, имя конфигурируемо настройкой DB_SCHEMA (OPENBAO__APP__DB_SCHEMA в контуре; MetaData, search_path, Alembic читают models.base.SCHEMA, в миграциях — from src.models.base import SCHEMA, не литерал); 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) и e2e (поверх integration: нужен tabular-parser из профиля services, иначе skip; make test-integration его исключает, гонять — make e2e); БД сессии — свежая 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 на files.tabular_parsed (по умолчанию; задаётся OPENBAO__NATS__NATS_*_SUBJECT). Обмен — 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 не пишут.
  • Первичные документы (Срез 2c) — services/primary_files.py (загрузка upload_one: дедуп по (ЮЛ, sha256) под advisory-lock, SAVEPOINT на файл, delete), v5.18 (#96): загрузка в консолидацию — check_consolidation_for_upload → accept_uploads (endpoints/files/upload.py, общий для обоих маршрутов) → after_upload_to_consolidation (отдельная транзакция, версии консолидации INITIATED с ЮЛ в периметре под FOR UPDATE ORDER BY id); рабочий стол — page_desk / files_repo.list_desk_files (OR EXISTS по consolidation_document); автозамена наследует consolidation_id; services/file_status.py (единственное место смены stored_file.status по ТЕКУЩЕМУ заданию; PROCESSED/SUPERSEDED обменом не перезаписываются), services/result_mapping.py (цикл маппинга: pick_candidate → проверка ключа результата (OQ-70) → чтение S3 вне транзакции → map_file в одной транзакции → автозамена FILE_REPLACE), services/documents.py (карточки, confirm), services/auto_confirm.py (автоподтверждение чистых документов в транзакции маппинга: _map_candidate: map_file → автозамена → auto_confirm → on_documents_mapped; confirmed_by NULL, DOCUMENT_CONFIRM без пользователя, переход версии — от uploaded_by; OQ-157), domain/osv_mapping.py (чистые функции: normalize_account_code, build_entries, counterparty_check, document_status), repositories/counterparties.py (CounterpartyIndex — резолв контрагента без policy). Фоновых циклов три (запускаются в lifespan при NATS_ENABLED; цикл маппинга — ещё и при подключённом S3 (storage)): публикатор outbox, вахтёр сроков и цикл маппинга (MAPPING_POLL_SECONDS, MAPPING_MAX_ATTEMPTS); inbox результат не читает и документов не создаёт. Порядок блокировок: задание → файл → документы (inbox, вахтёр, публикатор, маппер, confirm, delete; с Среза 4 далее — advisory-lock ГК → версии консолидации, см. «Консолидации») — иначе взаимоблокировки; автозамена дополнительно берёт advisory-lock по ЮЛ. Аудит: FILE_UPLOAD, FILE_REPLACE (системное, user_id пуст), FILE_DELETE, DOCUMENT_CONFIRM (пользователь или система при автоподтверждении); смена статусов обмена и маппинга не аудируется (OQ-55). osv_entry пишет только маппер, один раз на файл (очистка перед вставкой); TOTAL не хранится.
  • Формы ББ/ОФР (Срез 3b) — map_file (services/result_mapping.py) мапит и формы ББ/ОФР: domain/statement_mapping.py (чистые функции) + repositories/statements.py (financial_statement_meta/column/line/value); форма создаётся с тем, что есть в результате (колонки могут отсутствовать); второй BALANCE/INCOME того же файла пропускается с предупреждением (OQ-77), DISCLOSURE/OTHER — один документ без данных на тип (RECOGNIZED, D-3); документ BALANCE/INCOME без meta (создан до Среза 3b) подтверждается без условий D-9, …/statement по нему — 409 DOCUMENT_NOT_STATEMENT (решение №16). Проверки §7 — словарь и расчёт в пакете arm_statements (api/arm_statements/, копия, напрямую не править — ../docs/scripts/sync-statements.sh); копии словаря §7 в ядре нет (удалены api/rules/statement_checks.yaml, load_check_rules, run_checks). Нарушения при приёме результата и после правки пишет пакет через services/statements_engine.py и они хранятся в document_issue (relation / soft, codes, line_ids); ответ формы читает их оттуда. В domain/statement_checks.py остались только межформенная проверка 1370↔2400 (на лету, допуск — engine.tolerance()) и subline_parents (parent_code ответа). Акцепт — checks_accepted_by/at/comment в шапке. Правка формы и акцепт — services/statements.py (PATCH …/statement, POST …/statement/checks/accept): только до подтверждения (CONFIRMED → DOCUMENT_READONLY), аудит RECOGNIZED_DATA_EDIT (раздел RECOGNITION, object_type=document; один запрос — одно событие), первичка рядом (original_amount, *_original_value, is_modified/*_modified); акцепт сбрасывает только правка данных (подтверждения ячеек, кодов и дат — нет, Р-25), статус документа пересчитывается после каждой правки. Порядок блокировок «файл → документ», шапка (meta) читается ПОД блокировкой с refresh=True — иначе в сессии остаётся устаревший акцепт. Подтверждение формы (services/documents._check_statement) требует период (PERIOD_REQUIRED) и пройденные/акцептованные жёсткие проверки (CHECKS_NOT_PASSED, detail.failures); мягкие (в т. ч. межформенная 1370↔2400) не блокируют. Полный состав формы — domain/statement_lines.py по справочнику api/rules/statement_lines.yaml (редакции 66n/fsbu4 из шаблонов банка; это справочник имён и состава экрана — перечни кодов редакций живут в arm_statements (Rules.code_orders, engine.editions()/engine.code_order(...)), согласованность проверяет test_name_dictionary_codes_in_code_orders): GET …/statement достраивает строки и ячейки с id = null, PATCH по line_code создаёт строку (line_code_original_value = NULL — «не из источника»); пустая созданная строка вклада в §7 не даёт, а пустая цель соотношения при заполненных членах — нарушение с expected = NULL (P3-30, P1-21; потерявшая итоговую строку форма не подтверждается до правки или акцепта). Приём результата 2.0 (часть Б) — _load_result выбирает маппер по MAJOR schema_version (contract.versions.parse_result: 1 — RecognitionResult, 2 — RecognitionResultV2; иной MAJOR и битый/не-JSON файл — MappingFatal с итогом проверки схемы). Формы 2.0 (services/statement_intake_v2) пишутся как пришли: §7, needs_review и предупреждения берутся из результата, ядро добавляет только проверки info (core_info_issues) и страховку статуса (engine.contract_document_status, RESULT_STATUS_INCONSISTENT); предупреждения и нарушения сервиса — document_issue с source = service, первый же пересчёт после правки заменяет relation / soft записями core, прочее сохраняет. ОСВ 2.0 — по Р-28: статус листа из osvDocument.status, сошедшихся сумм, ошибки файла и предупреждений маппинга; предупреждения листа (OSV_*) — в document_issue документа ОСВ и на статус не влияют (P3-49). Ошибка файла — единый источник (P3-53, R-11): metrics.warnings результата 2.0 сохраняются в recognition_job.metrics.file_warnings, пересчёт читает их оттуда (без ключа — metrics.warnings конверта, 1.x). recompute_form/checks_on_intake_1x — services/statement_recompute.py, вход пакету собирает domain/stored_form.py (is_recognized = NULL — не сомнение, P3-54). Решения — openspec/changes/statement-template-lines/design.md. Комментарии к документу — services/documents.py (comments/add_comment, comment(object_type='document'), без аудита; comments_count в карточках). GET /files/{id}/content — services/primary_files.content + ObjectStorage.stream (поток из S3, первая порция до заголовков → FILE_CONTENT_NOT_FOUND; сессию тело потока не держит; имя — RFC 5987, X-Content-Type-Options: nosniff; освобождение потока — common/streaming.py: ClosingStreamingResponse + ChainedStream закрывают источник и при обрыве клиента). Скачивание первички, CSV и комментарии аудит не пишут (в отличие от экспорта сводной ОСВ в xlsx — тот пишет EXPORT; OQ-153). HeaderPatch.units — непустая строка после обрезки (пробельная — 422). GET /users/brief — current_user без права; documents_count у ЮЛ — repositories/directories; CSV чек-листа — ?format=csv у GET …/checklist. Контракт для фронта — ../docs/dev/api-contract-slice-3b.md; решения — openspec/changes/archive/2026-10-07-slice-3b-statements-api/design.md.
  • Консолидации (Срез 4) — api/endpoints/consolidations/ (periods, versions, locks, status, members, checklist, osv, export, mapping), сервисы: consolidations (создание, список, карточка, периметр PUT …/members), consolidation_guard (lock_visible_version/lock_editable_version: версия FOR UPDATE → видимость 404 → статус «в работе» → своя блокировка), consolidation_locks (взять/перехватить/снять; без истечения, OQ-119), consolidation_status (transition + ручной POST …/status), consolidation_documents (fits, bind_confirmed, unbind_entity, хуки on_document_confirmed(changed_by, comment) / on_documents_superseded / on_documents_mapped; bound_versions / unbind_documents — снятие привязок автоподтверждённых документов при удалении файла, OQ-157; for_consolidation — ВРЕМЕННЫЙ режим «период ОСВ из версии» CONSOLIDATION_PERIOD_FROM_VERSION, domain/version_period.py, arm-core#103: привязка, свод, чек-лист и osv_accounts читают период документа только через него), consolidated_osv (единая сборка _build: страница, сводка, кандидаты, экспорт), counterparty_mapping (ручное сопоставление, алиас, автоприменение), osv_export (openpyxl, лист «Пробелы»). Создание и PUT …/members проверяют периметр одной функцией _validate_perimeter (#42); ячейки чек-листа самоописывающие (document_type, account); members[].osv_accounts — repositories/consolidations.osv_accounts («что загружено к периоду версии», а не подходящесть §3). Правила: (1) статус версии меняет ТОЛЬКО consolidation_status.transition; версия к вызову уже под FOR UPDATE у вызывающего (иначе параллельный переход или перехват читает устаревший статус); (2) порядок блокировок: файл → документ → advisory-lock ГК → версии (по возрастанию id); правка версии начинается с consolidation_version FOR UPDATE; lock_versions_for_entity блокирует надмножество версий «в работе» (ЮЛ — член ИЛИ та же ГК), создание версии берёт per-ГК pg_advisory_xact_lock (R-2); (3) lock_document/lock_file_documents — FOR NO KEY UPDATE (R-3: вставка consolidation_document берёт FOR KEY SHARE на документ и взаимоблокировалась бы с FOR UPDATE); (4) сводная ОСВ, чек-лист и признак контрагента — чистые функции, в БД не пишутся; единственное хранимое — ручное сопоставление (counterparty_id + counterparty_match_kind + алиас); (5) ключ замены привязки — «тип, счёт, квартал, нарастающий» (R-1); ОСВ за Q1 равнозначна нарастающей и квартальной (R-14, Q1-эквивалентность — только для ОСВ, ББ/ОФР за Q1 версиям Q2–Q4 не подходят, R-14a); (6) «строка версии» для ручного сопоставления/автоприменения — только строки свода (R-11); (7) 404 — код NOT_FOUND, различие по тексту (в т. ч. «консолидация не найдена»); правки — ensure_version_editable (VERSION_NOT_EDITABLE/LOCK_REQUIRED/ LOCKED_BY_OTHER без locked_by_name). Деньги в ответах — строки с двумя знаками. Аудит: CONSOLIDATION_CREATE, CONSOLIDATION_STATUS_CHANGE, CONSOLIDATION_LOCK, CONSOLIDATION_LOCK_TAKEOVER, MEMBERSHIP_CHANGE, OSV_COUNTERPARTY_MAP, LEGAL_ENTITY_CHANGE, EXPORT; FILE_REPLACE.unbound_version_ids. Контракт для фронта — ../docs/dev/api-contract-slice-4.md; решения — openspec/changes/slice-4-consolidation/design.md.
  • Аудит — 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/kpp.py — КПП (9 символов, необязателен, у ИП запрещён, INVALID_KPP, v5.17); 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/; каждая сверяется с файлом docs своего MAJOR (*.v<M>.schema.json, examples/v<M>/, если он есть, иначе основной), правило — tests/contract_identity.py (одинаковый файл в трёх репозиториях), тест — 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.22.dbml (текущая версия; v5.22 структуру не меняла; миграции 0001_dbml_v5_13 + 0002_dbml_v5_14 + 0003_dbml_v5_15 + 0004_dbml_v5_16 + 0005_dbml_v5_17 + 0006_dbml_v5_18 + 0007_dbml_v5_19 + 0008_dbml_v5_20 + 0009_dbml_v5_21); api/scripts/check_dbml_vs_orm.py (make dbml-check, входит в check-full) — обязательная проверка.
  • Алгоритмы — только из ../docs/context/05-domain-rules.md; номер § — в docstring.
  • Вычисляемые объекты консолидации — api/src/domain/, без ORM-таблиц.
  • Фронт (web/) разрабатывается здесь (с 21.10.2026): фронтовые задачи — worktree arm-core и PR в main, как и бэкенд. Отдельный репозиторий RSHB/arm-web заморожен на test de4e477 (= arm-core 3157a15), синхронизация make -C ../docs sync-web больше не используется; правило «web/ напрямую не править» отменено. Проверки фронта — make web-check (biome + tsc + build, входит в check-full) и из web/: npm test (vitest), npm run lint (ESLint + Prettier), форматирование — npm run format. Спеки экранов — openspec/specs/*-ui (перенесены из arm-web@de4e477); новые и изменённые требования к экранам — в openspec/changes/ этого репозитория.
  • Фронт: пользователь слаб во фронте — для TS/React изменений делай ревью сам и объясняй решения в PR.
  • api/arm_statements/ — копия пакета arm-statements, напрямую не править: обновление — ../docs/scripts/sync-statements.sh (--in-place в PR фичи), свежесть — make statements-check (в check-full).
  • OpenSpec: openspec/ здесь; сквозные изменения — change в ../docs.