Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_012vJkmBGKKqMoEMzvsXG3y2
23 KiB
23 KiB
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 по словарюapi/rules/statement_checks.yaml),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(MetaData,search_path, Alembic); PKuuidчерезUUIDPrimaryMixin; ревизии вapi/alembic/versions/; автогенерация — черновик, финал сверяется с DBML скриптом. - Ошибки —
common/exceptions.py→core/exception_handlers.py. Доменные ошибки (DomainError:NotFoundError404NOT_FOUND,ConflictError409,DomainValidationError422; код и доп. поля задаёт вызывающий) отдаются как{"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); frontendbiome(ширина 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 grouparm-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. Статусы задания в Срезе 0audit_eventне пишут. - Первичные документы (Срез 2c) —
services/primary_files.py(загрузкаupload_one: дедуп по(ЮЛ, sha256)под advisory-lock, SAVEPOINT на файл,delete),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),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безmeta(создан до Среза 3b) подтверждается без условий D-9,…/statementпо нему — 409DOCUMENT_NOT_STATEMENT(решение №16). Проверки §7 —domain/statement_checks.pyпо словарюapi/rules/statement_checks.yaml; файл — копия словаря tabular-parser, при изменении правится в обоих репозиториях (расхождение молчаливо меняет результат проверок). Проверки считаются на лету и не хранятся; хранится только акцепт (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); любая правка сбрасывает акцепт и пересчитывает статус документа. Порядок блокировок «файл → документ», шапка (meta) читается ПОД блокировкой сrefresh=True— иначе в сессии остаётся устаревший акцепт. Подтверждение формы (services/documents._check_statement) требует период (PERIOD_REQUIRED) и пройденные/акцептованные жёсткие проверки (CHECKS_NOT_PASSED,detail.failures); мягкие (в т. ч. межформенная 1370↔2400) не блокируют. Комментарии к документу —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).GET /users/brief—current_userбез права;documents_countу ЮЛ —repositories/directories; CSV чек-листа —?format=csvуGET …/checklist. Контракт для фронта —../docs/dev/api-contract-slice-3b.md; решения —openspec/changes/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/on_documents_superseded/on_documents_mapped),consolidated_osv(единая сборка_build: страница, сводка, кандидаты, экспорт),counterparty_mapping(ручное сопоставление, алиас, автоприменение),osv_export(openpyxl, лист «Пробелы»). Правила: (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/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(AuthenticationFailedError401/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/(envARM_KA_FIXTURES); при отсутствии тесты с маркеромfixturesпропускаются с явной причиной (skip reason). - NATS subjects и схема ключей S3 —
../docs/context/03-architecture.md.
Правила репозитория
- ORM 1:1 с
../docs/model/data-model-v5.16.dbml(текущая версия; миграции0001_dbml_v5_13+0002_dbml_v5_14+0003_dbml_v5_15+0004_dbml_v5_16);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.