arm-core/CLAUDE.md
Andrey Epifancev b4522addc0 feat: DBML v5.23 — версия правил обязательна; список пар ВГО версии (ревью #175, AURORA-1536)
- consolidation_version.elimination_rules_version NOT NULL: миграция 0010
  проставляет существующим версиям номер правил, действовавших на дату
  создания (по Москве), иначе последних открытых; без правил — отказ;
  создание версии без действующих правил — 409 ELIMINATION_RULES_MISSING;
  ветки «правила не зафиксированы» убраны (rules, preset, is_version_fixed
  всегда true);
- GET …/elimination-totals: все тройки «правило ББ × упорядоченная пара
  периметра» с ΣA, ΣB, расхождением, суммой элиминации, статусом
  AUTO/DISCREPANCY/ONE_SIDED/MANUAL и osv_entry_ids; сводная ОСВ строится
  один раз; сбор строк сторон вынесен в _collect_rule_sides — общий
  с упаковкой тройки.

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

190 lines
31 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`
(только цикл миграций), `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.23.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` + `0010_dbml_v5_23`); `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`.