Решение владельца 08.10.2026: запросы — files.tabular_uploaded / files.img_uploaded
(было recognition.{tabular|image}.request), ack и результаты обоих сервисов —
files.tabular_parsed (было recognition.result); files.img_parsed не используется,
старый АРМ гасится. Тела сообщений, Nats-Msg-Id, queue group'ы и schema_version
не меняются.
Change nats-files-subjects: дельта recognition-contract (3 требования), Purpose
спеки; описания subject'ов в contracts/*.schema.json; 03-architecture (схема,
таблица потоков, NATS платформы), 09-delivery-plan, dev/README, dev/qa-guide,
repo-templates. DBML v5.17 не поднимается (R-3, deferred).
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_012vJkmBGKKqMoEMzvsXG3y2
52 KiB
03. Архитектура (целевая, по решениям DECISION-1…5 и ARCH-* в DBML v5.17)
Компоненты (= репозитории, см. 00-workspace-layout.md)
┌──────────────┐ JWT (email) ┌──────────────────────────────┐
браузер ──────▶│ React SPA │ ───────────────▶│ arm-core (FastAPI) │
└──────────────┘ │ REST /api/v1, policy-слой, │
│ единственный писатель в БД │
└───────┬──────────┬───────────┘
│ │
PostgreSQL │ │ S3 (uploads/ results/ exports/ archive/)
│ │
NATS (core) ◀─────────────────┘ outbox │
files.tabular_uploaded │
files.img_uploaded │
files.tabular_parsed ─────────────────────▶ arm-core (inbox, дедуп по msg_id)
│
┌───────────────┴───────────────┐
┌────────▼────────┐ ┌─────────▼────────┐ ┌──────────────────┐
│ tabular-parser │ │ scan-gateway │ ◀─────▶│ ИС Сканирование │ (внешняя, горячие папки)
│ XLS/XLSX/XLSB │ │ PDF/PNG/JPG │ └──────────────────┘
│ stateless, без БД│ │ stateless, без БД│
└─────────────────┘ └──────────────────┘
- arm-core — монолитное ядро: справочники, файлы/документы, маппинг результатов распознавания, консолидация (вычисления), согласование, ЕХ, аудит, права. Один Alembic, одна схема.
- tabular-parser — читает файл из S3
uploads/, находит на листах ОСВ (§1) и формы ББ/ОФР (§12) по содержимому, пишет нормализованный JSON в S3results/, публикуетfiles.tabular_parsed. Внутри —BookParser(оркестратор книги: лимиты, чтение, дедлайн, вид каждого листа): листы ОСВ идут в конвейерosv.py, листы форм — в конвейер формstatements/(блоки форм, колонки, шапка, строки, соотношения §7, склейка продолжений); документы результата — сначала ОСВ, затем формы. Контракт результата —docs/contracts/recognition-result.schema.json(контракт v1.2:DISCLOSUREвdocument_type,statementDocument.checks, границы периода ОСВperiod_start/period_end/is_from_year_start). Правила распознавания заголовков/группировок —rules/*.yaml. - scan-gateway — адаптер ИС Сканирование: положить файл в горячую папку
Input/<job_id>.<ext>(расширение — суффиксoriginal_nameв нижнем регистре; временный.partпишется вTmp, а не вInput(решение владельца 06.10.2026) и публикуется во входящий каталог без замены; публикация сама снимает.partнезависимо от флага очистки, вTmpостаются только осиротевшие.partи дубли), дождаться набораOutput/<job_id>/*.json(по одному json на документ; набор готов, когда имена и размеры не менялисьSCAN_SETTLE_SECONDS), сохранить склеенный сырой ответ в S3raw/{job_id}.json(в БД не регистрируется), очиститьOutput/<job_id>/и входящий файл — только под флагомSCAN_CLEANUP, по умолчанию выключен (права на удаление на SMB-шаре контура не подтверждены, OQ-144), нормализовать в тот же контракт результата (OcrNormalizerпо05-domain-rules.md§11/§11.1: одинstatementDocumentна таблицу ответа, справочникиapi/rules/с редакциями форм), опубликоватьfiles.tabular_parsed. Таймаут 5 мин →TIMEOUT. Раскладка папок — как у проверенного в контуре PDFWorker старого АРМ (RSHB/armka_file_worker, решение владельца 06.10.2026, OQ-137) с двумя отличиями: временный файл лежит вTmp, а не рядом с назначением, очистка под флагом. Эмулятор ИС Сканирование (dev) ищет пару PDF + JSON по sha256 содержимого входящего файла, а не по имени. Реализация — arm-scan-gateway #6 (changeslice-3a-ocr-normalizer). - Frontend — React + TS по шаблону команды; экраны по макету
dkb_24.09.26(справочник ГК, консолидации, первичные документы, проверка распознавания, сводная ОСВ, шахматка, корректировки, гудвилл, КФО, ЕХ, журнал, роли).
Контракт обмена через NATS (DECISION-4)
Схемы — docs/contracts/ (v1.0), JSON Schema 2020-12: recognition-request.schema.json
и recognition-result-message.schema.json — тела сообщений,
recognition-result.schema.json — файл результата в S3; примеры —
docs/contracts/examples/. Примеры проверяются схемами: python scripts/validate_contracts.py
(из корня репозитория docs). Примеры — иллюстрации формы сообщений, а не готовые к исполнению задания:
схема проверяет у reply_deadline только формат date-time и не сравнивает срок с текущим временем, поэтому
request-image.json проходит схему при reply_deadline в прошлом. Срок проверяет сервис: scan-gateway отвечает
error TIMEOUT, если reply_deadline истёк до отправки файла в ИС Сканирование; tabular-parser срок не проверяет.
Файлы examples/ копируются в сервисы побайтно, поэтому пример не правится ради «свежей» даты.
| Поток | Subject | Кто пишет | Полезная нагрузка | Nats-Msg-Id |
Схема |
|---|---|---|---|---|---|
| запрос | files.tabular_uploaded / files.img_uploaded |
arm-core (outbox из recognition_job) |
schema_version, job_id, file_id, pipeline, file_type, original_name, s3 {bucket, key} исходника, reply_deadline, attempt |
<job_id>:<attempt> |
request |
| ack | files.tabular_parsed |
tabular-parser / scan-gateway | конверт со schema_version, status = processing, job_id, file_id, service {name, version} |
ack:<job_id>:<attempt> |
message |
| результат | files.tabular_parsed |
tabular-parser / scan-gateway | schema_version, job_id, file_id, status (done / needs_check / error), service {name, version}, metrics (опц.), raw_s3_key (опц., только scan-gateway); при done / needs_check обязательны result_s3 {bucket, key} и result_schema_version, при error — error {code, message} |
result:<job_id>:<attempt> |
message; файл в S3 — result |
group_schema в конверт не входит — ядро берёт его из documents[].group_schema файла результата при маппинге.
На платформе NATS без JetStream (ответ 03.10.2026; подтверждено 06.10.2026 кодом RSHB/armka_file_worker, который
работает в контуре на subscribe/publish + flush без jetstream()): нет durable consumer'ов, серверного дедупа и redelivery.
Поэтому гарантии доставки — на уровне приложения: сервисы подписаны queue group'ами (tabular-parser, scan-gateway),
ядро — queue group arm-core на files.tabular_parsed; сервис сразу по получении корректного запроса публикует ack processing;
ядро, не получив ack за ACK_TIMEOUT_SECONDS (30), публикует запрос повторно с тем же Nats-Msg-Id, не более
OUTBOX_MAX_REPUBLISH (3) раз, затем dlq. Сервис держит process-local множество job_id в работе и на повторный
запрос отвечает ack'ом без второй обработки. Публикация ack и результата сервисом при временной ошибке повторяется
с тем же Nats-Msg-Id: первая попытка плюс PUBLISH_RETRIES повторов (по умолчанию 3, всего до 4 попыток); повторы
исчерпаны — error-лог, задание закроет ядро (по reply_deadline). Появится JetStream — меняются только publisher/subscriber.
Правила:
recognition_jobсоздаётся в статусеqueuedв одной транзакции соstored_file; фоновый публикатор переводит вpublished(request_msg_id=Nats-Msg-Id), ack сервиса — вprocessing(started_at). Повтор публикации — поqueuedстарше N секунд и поpublishedбез ack.- Результат принимается подпиской queue group;
result_msg_id UNIQUE= дедуп. Нет ack после повторов →dlq; потерянный результат →reply_deadline→timeout; replay = сброс вqueuedсattempt + 1. - Результат одного файла содержит N документов; ядро на каждый создаёт
documentи маппит данные. - Три формы
Nats-Msg-Id: запрос<job_id>:<attempt>, ackack:<job_id>:<attempt>, результатresult:<job_id>:<attempt>. Ack (processing) сервис публикует до чтения файла из S3; повторная публикация запроса ядром идёт с тем жеNats-Msg-Idи тем жеattempt. Дубликат результата ядро определяет поrecognition_job.result_msg_id. schema_version—MAJOR.MINOR; ядро и сервисы принимают тот же MAJOR и любой MINOR. Сервис отвергает запрос с неизвестным MAJOR (как и запрос не по схеме) результатомerrorс кодомSCHEMA_MISMATCHбез предшествующего ackprocessing: финальный конверт сам закрывает задание в ядре (published → error), повторной доставки не будет. Конверт публикуется, только если из тела извлекаются корректныеjob_idиfile_id; дляjob_id, уже находящегося в работе или обработанного, он не публикуется (егоNats-Msg-Idсовпал бы с настоящим результатом). Нечитаемый запрос (не JSON, нетjob_id) остаётся без ответа: сервис пишет лог и отбрасывает его, ядро не получает ack, повторяет публикациюOUTBOX_MAX_REPUBLISHраз и закрывает задание вdlqсerror_code = NO_ACK. Новый MINOR раскатывается сначала на потребителей (ядро и сервисы обновляют копии схем раньше, чем производитель начнёт слать новый MINOR, например 1.2), потому что закрытые enum отвергают незнакомые значения.- Сервисы не знают про БД и про бизнес-смысл: они возвращают структуру файла; нормализация счетов, резолв контрагентов, проверки — в ядре.
Цикл маппинга результата и порядок блокировок (Срез 2c)
Обработка результата разделена на два шага, чтобы подписка NATS не ждала S3 и тяжёлые вставки (иначе ack следующих
сообщений не успевает за ACK_TIMEOUT_SECONDS, OQ-69):
- Inbox (
handle_message, в callback подписки) — быстрый: дедуп поresult_msg_id, регистрация файла результата вs3_object, статус задания (done/needs_check/error), статус файла по заданию (PROCESSING/ERROR). Файл результата не читает и документов не создаёт. - Цикл маппинга (
services/result_mapping.py:run_mapping_loop, фоновая задача lifespan наравне с публикатором outbox и вахтёром сроков — всего три цикла; периодMAPPING_POLL_SECONDS= 5) — берёт кандидатаdone/needs_check, чей файлPROCESSINGи для которого это текущее задание (FOR UPDATE SKIP LOCKED), проверяет ключ результата (бакет хранилища и префиксresults/{file_id}/{job_id}/, OQ-70), читает объект из S3 вне транзакции, валидируетRecognitionResultv1.x и в одной транзакции создаёт документы и строки, переводит файл вPROCESSEDи выполняет автозамену. Повторяемые сбои — доMAPPING_MAX_ATTEMPTS(счётчик вrecognition_job.metrics.mapping_attempts), затем файлERROR(metrics.mapping_error). Алгоритм —05-domain-rules.md§9.5; чистые функции —domain/osv_mapping.py.
Статус файла (stored_file.status) меняет только код, проверивший current_recognition_job_id == job.id
(services/file_status.py); результат устаревшего задания файл не трогает; PROCESSED/SUPERSEDED обменом не
перезаписываются.
Порядок блокировок: задание → файл → документы. Его соблюдают все участники: inbox, вахтёр, публикатор (dlq), цикл
маппинга (после выбора кандидата блокирует файл), confirm (файл, затем документ, оба перечитываются под FOR UPDATE) и
DELETE /files/{id} (все задания файла по id, затем файл и документы). Автозамена одного ЮЛ дополнительно берёт
pg_advisory_xact_lock по ЮЛ (иначе два параллельно смапленных файла с одним документом не увидят друг друга), загрузка —
advisory-lock по (ЮЛ, sha256) (двойной клик не проскакивает дедуп).
Консолидации и сводная ОСВ (Срез 4)
Версия консолидации — единица работы аналитика: периметр (consolidation_member), комплект документов
(consolidation_document, явная привязка), статус, блокировка «взять в работу». Сводная ОСВ, чек-лист, признак контрагента и
пробелы — чистые функции от версии (DECISION-5, ничего не кэшируется в БД и в процессе).
- Чистый домен свода (D-1).
repositories/consolidations.py:load_osv_rowsодним запросом отдаёт строки привязанных ОСВ-документов;domain/consolidated_osv.py:buildиdomain/consolidation_periods.py:select_sources(без БД и сессий, mypy strict) выбирают источники, склеивают кварталы, находят пробелы, резолвят контрагента, применяют фильтры.services/consolidated_osv.py:_build— единственная сборка, которую делят страница (GET …/osv), сводка, кандидаты, ручное сопоставление (ответ) и экспорт xlsx (services/osv_export.py). Весь свод версии строится в памяти на запрос (замер синтетического тестаtest_large_version_is_fast_without_n_plus_one: 5,4 тыс. исходных строк → 1,8 тыс. строк свода — 0,27 с, 8 запросов; на фикстурах РБ — 1385 строк свода, 22 документа; порог для кэша — OQ-121). - Пакет консолидаций.
api/endpoints/consolidations/(periods, versions, locks, status, members, checklist, osv, export, mapping); сервисыconsolidations(создание, список, карточка, периметр),consolidation_guard(общий пролог правок: версияFOR UPDATE→ видимость → статус «в работе» → своя блокировка),consolidation_locks,consolidation_status,consolidation_documents(привязка/отвязка),consolidated_osv,counterparty_mapping,osv_export; домен —policy(ensure_version_editable),checklist,consolidation_periods,consolidated_osv. - Единственное место смены статуса —
consolidation_status.transition: ручнойPOST …/statusи автоматические переходы (загрузка с версией, маппинг, подтверждение документа) зовут её же, поэтому история и аудит одинаковы. Версия к моменту вызова уже подFOR UPDATEу вызывающего. - Порядок блокировок: файл → документ → advisory-lock ГК → версии (по возрастанию
id). Подтверждение документа берёт файл, документ и затем все версии «в работе» ГК ЮЛ (lock_versions_for_entityблокирует надмножество — член ИЛИ та же ГК — и только потом отбирает версии, где ЮЛ член, иначе confirm ∥PUT …/membersтеряет привязку: R-2); создание версии берётpg_advisory_xact_lockпо ГК; правка версии начинается сconsolidation_version FOR UPDATE. Документы блокируютсяFOR NO KEY UPDATE(R-3): вставкаconsolidation_documentберётFOR KEY SHAREна документ и конфликтовала бы сFOR UPDATE. Ручное сопоставление: версия → advisory-lock по нормализованному алиасу (его берёт иaliases.addсправочника) → алиасы → строкиosv_entryс тем же именем. - Нагрузка и транзакции. Переход версии после загрузки выполняется в отдельной короткой транзакции
(
session_transaction), сбой не отменяет принятые файлы (R-10). Чтение (GET …/osv, сводка, экспорт) блокировок не берёт.
Формы ББ/ОФР, проверки §7 и API экрана проверки (Срез 3b)
Формы (statementDocument контракта v1.2) приходят от обоих сервисов распознавания в одном формате; ядро — единственное
место, где они становятся строками БД и где считаются проверки §7 (самопроверку checks кладёт в результат только tabular-parser, scan-gateway — нет; ядро её игнорирует).
- Маппинг форм — ветка
map_file, а не отдельный цикл. После листов ОСВ тот же вызовmap_fileсоздаёт документыBALANCE/INCOMEиfinancial_statement_meta/column/line/valueв той же транзакции (порядок: очистка прежних данных файла, включаяfinancial_statement_*его документов → документы ОСВ и форм → строки → статус файла → автозамена →on_documents_mapped). Отдельный цикл делил бы файл, его статус и автозамену между двумя транзакциями.MappingFatal— только при пустомdocuments. Чистые функции —domain/statement_mapping.py(черновики документа, шапки, колонок, строк и значений; период по дате отчёта либо по самой поздней дате колонок; статус документа §9.3); правила —05-domain-rules.md§9.5 п. 2. - Проверки §7 — чистые функции без БД.
domain/statement_checks.py(run_checks,cross_form_check) — перенос логикиtabular-parser/api/src/parser/statements/checks.py; словарь —arm-core/api/rules/statement_checks.yaml, копияtabular-parser/api/rules/statement_checks.yamlс пометкой источника. Теста идентичности копий между репозиториями нет намеренно (урок arm-core #33: тест, читающий соседний репозиторий, ломал main). Риск: копии могут разойтись незаметно; при изменении словаря в tabular-parser правку нужно повторять в ядре вручную и наоборот. Результат не хранится: считается по текущим суммам на каждыйGET …/statement, правку и подтверждение; хранится только акцепт (financial_statement_meta.checks_accepted_by/at/comment). - Слои API.
api/endpoints/files/—statements.py(GET/PATCH /documents/{id}/statement,POST …/statement/checks/accept),comments.py(POST/GET /documents/{id}/comments),read.py(GET /files/{id}/content); сервисservices/statements.py(чтение, правка, акцепт, пересчёт статуса),services/documents.py(подтверждение:PERIOD_REQUIRED,CHECKS_NOT_PASSED),repositories/statements.py; схемы —schemas/statements.py,schemas/comments.py;GET /users/brief—api/endpoints/users.py. Правка и акцепт берут блокировки в общем порядке «файлFOR UPDATE→ документFOR NO KEY UPDATE» и перечитывают шапку под блокировкой. - Исходный файл отдаётся ядром потоком из S3 (
miniopy-async, чтение частями, без загрузки целиком в память): прямая ссылка на MinIO не используется, потому что в контуре клиент может не видеть хранилище, а через ядро действует видимость по ЮЛ. Объекта нет в хранилище (убран ретеншном S3 или пропал) — 404FILE_CONTENT_NOT_FOUND; S3 не подключено — 503STORAGE_UNAVAILABLE. - Документы Среза 3b не меняют модель: DBML v5.16, миграций и значений enum нет; контракт
docs/contracts/не менялся (редакция формы66n/fsbu4выводится ядром из даты, OQ-145).
Схема ключей S3
Один бакет (OPENBAO__MINIO__MINIO_BUCKET шаблона); роль объекта — первый сегмент ключа:
{bucket_role}/{file_id}/{job_id}/{name}. Роли: uploads (исходники, срок — весь срок хранения), results
(нормализованные JSON; ретеншн — ARCH-010, открыт), exports (7 дней), archive (data lake). Все объекты регистрируются
в s3_object (bucket_role, bucket, key, sha256, size, expires_at). Сырой ответ OCR raw/{job_id}.json — по
конвенции, без записи в БД. Lifecycle-правила — по префиксам.
Срез 2c: ядро кладёт исходник по ключу uploads/{file_id}/{job_id}/{безопасное имя} (job_id задаётся заранее — ключ
известен до записи в БД; имя очищается от путей и управляющих символов); results/{file_id}/{job_id}/… пишет сервис
распознавания, а ядро перед чтением проверяет, что ключ лежит именно в папке задания (OQ-70). При удалении файла объектам
uploads и results проставляется s3_object.expires_at = now(), физически они не стираются (ретеншн — OQ-111, ждёт
lifecycle-правил платформы); объект, записанный в S3 до сбоя БД, остаётся сиротой (то же правило).
Аутентификация и авторизация
- SPA получает JWT от РАИСА; каждый запрос несёт
Authorization: Bearer. - arm-core валидирует подпись/срок, извлекает email, находит
app_user(is_active); нет пользователя — 403. - Права:
effective_permissions = ∪ role_permission(roles(user)). Проверка в зависимости FastAPIrequire(permission_code). - Policy-слой поверх прав (отдельный модуль, чистые функции): видимость по филиалу (
regional_office), по ЗО (business_segment), допустимость действия по статусу версии (consolidation_status) и по активной блокировке (consolidation_lock). Право говорит «может», policy — «эту ли и сейчас ли». - Локальная разработка: заглушка-эмитент JWT с теми же claims (feature-flag
AUTH_DEV_ISSUER). - Контур банка — временно тоже на заглушке (
DEBUG=true+AUTH_DEV_ISSUER=true, вход по email черезPOST /api/v1/dev/token): параметры JWT РАИСА неизвестны (OQ-54), платформенной аутентификации в контуре пока нет. Подробности и последствия —dev/contour-transfer-guide.md, раздел 6.3.
Слои arm-core (по раскладке шаблона команды)
Структура — раскладка шаблона api/src/ (см. «Соглашения шаблона» ниже); tabular-parser и scan-gateway — те же
каталоги без web/, db/, models/, alembic/. Соответствие прежним названиям этого документа: backend/app/ →
api/src/, integrations/ → core/, migrations/ → api/alembic/, frontend/ → web/.
api/src/
api/endpoints/ # роутеры FastAPI по контекстам: me, directories, files, documents, consolidation, kfo, ex, audit, users
api/dependencies/ # фабрики Depends: сессия, сервисы, current_user, require(permission)
domain/ # чистая предметная логика без I/O: правила ОСВ, периоды, матчинг, элиминация, проверки, пороги,
# машины статусов, policy — pydantic-модели на входе и выходе
services/ # use-cases: оркестрация domain + repositories + outbox + audit_event
repositories/ # SQLAlchemy-запросы, в т. ч. запросы-вычисления (сводная ОСВ, шахматка)
models/ # ORM 1:1 с DBML v5.17; имена таблиц/колонок/enum — как в DBML
schemas/ # pydantic DTO API /api/v1
core/ # config, clients (S3; NATS outbox-publisher и result-consumer), auth (JWT РАИСА / dev-эмитент),
# logger, exception_handlers
db/ # engine, сессия
api/alembic/ # миграции
api/scripts/ # check_dbml_vs_orm.py, seed
api/tests/ # фикстуры — из ../fixtures (ARM_KA_FIXTURES)
web/ # SPA
openspec/
Принципы: domain/ тестируется без БД на фикстурах из ../fixtures; вычисляемые объекты консолидации возвращаются
как pydantic-модели и не имеют ORM-таблиц; все мутации идут через services/ с записью audit_event.
Статусы и машины состояний
Три независимые машины: file_status (техника файла), document_status (проверка аналитиком), consolidation_status
(маршрут КФО) + recognition_status (жизненный цикл задания). Переходы — 05-domain-rules.md §9. Реализовать как
явные таблицы переходов в domain/, а не if-ы по роутерам.
Соглашения шаблона команды
Три сервисных репозитория развёрнуты из нулевого коммита репозитория команды RSHB/DKB_ARMKA (ba172f8, «Шаблон
FastAPI» платформы РАИСА/aurora): дерево импортировано как есть (arm-core@ba92222), доменный код удалён, каркас
оставлен (8e1f423). Документы подстраиваются под шаблон, а не наоборот. Ниже — что шаблон задаёт и что мы к нему
добавляем. Один и тот же текст действует для всех трёх репозиториев; отличия сервисов — в разделе «Что вырезано».
Раскладка репозитория
<repo>/
README.md описание шаблона (в Срезе 0 заменяется описанием сервиса)
raisa_config.yml профиль ресурсов платформы (cpu/ram); читает платформа при деплое
setup/ бутстрап разработчика: setup.sh (venv + pip + npm i); pip.conf и .npmrc → внутренний Nexus банка
api/ backend. РАБОЧИЙ КАТАЛОГ для всех команд: от него ищутся .env, pytest.ini и web/ для раздачи SPA
run_server.py точка входа платформы: uvicorn, host/port из SERVER__HOST / SERVER__PORT, lifespan=on
requirements.txt прод-зависимости, pinned (fastapi 0.135, SQLAlchemy 2.0.36, alembic 1.14, asyncpg, pydantic 2.13,
pydantic-settings, nats-py 2.14, miniopy-async, smbprotocol, loguru, sentry-sdk, raisa_fastapi_protected_api)
requirements-dev.txt dev-зависимости (pytest-asyncio, pytest-env, openpyxl, …) — пины рассинхронизированы с requirements.txt
pytest.ini asyncio_mode=auto, loop scope session, env APP_ENV=test / ENV_FILE=.env.test, testpaths=tests
alembic.ini, alembic/ env.py берёт URL из настроек приложения, схема dkb, compare_type=True, include_schemas
src/
__init__.py экспортирует app и get_settings
app.py FastAPI: lifespan (SMB-сессия, MinIO, NATS), middleware (gzip, proxy headers, CORS, РАИСА при DEBUG=false),
GET /healthcheck, include_router, раздача SPA из api/web/ (SPAStaticFiles, fallback index.html)
api/router.py агрегирующий APIRouter; api/endpoints/* — роутеры; api/dependencies/* — фабрики Depends
core/config.py pydantic-settings (см. «Конфигурация»); модуль создаёт settings при импорте
core/clients.py клиенты инфраструктуры (S3Storage поверх miniopy-async)
core/nats_callback.py обработчики сообщений NATS
core/logger.py loguru: setup_logger, перехват std logging и uvicorn, request_id, шумовые пути
core/exception_handlers.py ошибки → JSON {"detail": …}
common/exceptions.py доменные исключения (NotFoundError, CouldntCreateError)
db/db.py async engine (asyncpg), search_path=dkb, get_async_session
models/base.py DeclarativeBase с MetaData(schema="dkb"), UUIDPrimaryMixin (uuid4 на стороне приложения)
schemas/ pydantic DTO и enum'ы API
repositories/ запросы: класс на таблицу, принимает AsyncSession
services/ use-cases
tests/conftest.py
web/ frontend (остаётся только в arm-core): React 18 + TS, rsbuild, antd, Redux Toolkit, biome
Наши дополнения к api/src/, не меняющие существующих каталогов: domain/ — чистая предметная логика без I/O
(правила §1–§10, машины статусов, policy); core/auth.py — JWT РАИСА и dev-эмитент; api/scripts/ — служебные скрипты
(check_dbml_vs_orm.py, seed). Пути в документах вида rules/*.yaml, scripts/…, tests/… отсчитываются от api/.
Конфигурация
pydantic-settings, файлapi/.env, вложенность через__, лишние переменные игнорируются. Все инфраструктурные секреты — под префиксомOPENBAO(в проде заполняет OpenBao):OPENBAO__DATABASE__DATABASE_URL,OPENBAO__MINIO__{MINIO_URL, MINIO_ADMIN_LOGIN, MINIO_ADMIN_PASSWORD, MINIO_BUCKET},OPENBAO__NATS__{NATS_URL, NATS_SEED, NATS_JWT},OPENBAO__SMB__{SMB_HOST, SMB_PATH, SMB_LOGIN, SMB_PASSWORD}. Прочее:DEBUG,ROOT_PATH,VERSION,SERVER__HOST,SERVER__PORT.- Платформа РАИСА передаёт в под только секреты OpenBao, поэтому любая настройка верхнего уровня
Settingsв контуре задаётся какOPENBAO__APP__<ИМЯ>(приоритет: обычный env →OPENBAO__APP__*→.env→ умолчание; неизвестное имя — ошибка старта;OPENBAO__APP__OPENBAO__…запрещено). Контурные значения —dev/contour-transfer-guide.md, раздел 6. settingsсоздаётся при импорте (core/config.py,db/db.py,app.py) — импорт почти любого модуля требует полного окружения; тесты получают его из.env.test.DEBUG=true— режим разработки шаблона: NATS не подключается, middleware РАИСА не импортируется. Наши флаги разработки (AUTH_DEV_ISSUER,SCAN_EMULATOR, режим горячих папок) добавляются так же — отдельными полямиSettings, по умолчанию выключены; в проде ни один не включается.
База данных и миграции
- SQLAlchemy 2.0 async + asyncpg. Одна схема PostgreSQL
dkb— зашита в шаблоне (MetaData,search_path,version_table_schemaиinclude_nameAlembic). Следуем шаблону, неpublic. - Alembic берёт URL из настроек, не из
alembic.ini; ревизии —api/alembic/versions/, имя файла стандартное<rev>_<slug>.py. Автогенерация — только черновик: итоговую ревизию сверяет с DBMLapi/scripts/check_dbml_vs_orm.py. - PK везде
uuidчерезUUIDPrimaryMixin; имена таблиц, колонок и enum'ов — ровно как в DBML v5.17.
Инфраструктурные клиенты
- S3 —
miniopy-async(Minio,cert_check=False), один бакетMINIO_BUCKET; ролиs3_object.bucket_roleстановятся первым сегментом ключа (uploads/…,results/…,raw/…,exports/…,archive/…). - NATS —
nats-py; аутентификация nkeys-seed + JWT (NATS_SEED,NATS_JWT) черезuser_jwt_cb/signature_cb; соединение и подписки создаются в lifespan и живут вapp.state.nats/app.state.nats_subs. Core NATS без JetStream: подписки с queue group, заголовкиNats-Msg-Idставим сами, ack и повторы — на уровне приложения. Учётные данные одни на все сервисы АРМ: один JWT с правами по subject'ам (ответ владельца 06.10.2026). Subject'ы — как в старом АРМ (решение владельца 08.10.2026): на проде заведеныfiles.tabular_uploaded,files.img_uploaded,files.tabular_parsed,files.img_parsed; используем три — запросы вfiles.tabular_uploadedиfiles.img_uploaded, ack и результаты обоих сервисов в общийfiles.tabular_parsed;files.img_parsedне используется. Старый АРМ гасится, общих подписчиков на этих subject'ах у нас с ним не будет. - SMB (горячие папки ИС Сканирование) —
smbprotocol/smbclient, сессия регистрируется в lifespan; нужен толькоscan-gateway. - Внешняя аутентификация — пакет
raisa_fastapi_protected_apiиз внутреннего Nexus (AuthorizationMiddleware,ProtectedSettings, …), импортируется только приDEBUG=false(в контуре временноDEBUG=true, см. «Аутентификация и авторизация», п. 6); фронтовая пара —raisa-fastapi-protected-web. Вне сети банка оба пакета недоступны (см. вопросы Среза 0).
DI, слои, ошибки, логи
- Контейнера DI нет: ручные фабрики
Dependsвapi/dependencies/— фабрика получаетAsyncSessionизget_async_session, собирает репозитории и сервис; инфраструктурные клиенты берёт изrequest.app.state(storage,nats). - Поток запроса:
api/endpoints→services→repositories→models; DTO —schemas/. Доменные исключения изcommon/exceptions.pyмаппятся в HTTP вcore/exception_handlers.py(RequestValidationError → 422, IntegrityError → 409, SQLAlchemyError → 500, HTTPException как есть, прочее → 500); тело всегда{"detail": …}, тексты по-русски. - Логи — loguru (
core/logger.setup_logger): перехват std logging и uvicorn,request_idв extra, успешные обращения к/healthcheck,/docs, статике не логируются.
Тесты
- pytest + pytest-asyncio (
asyncio_mode=auto, loop scopesession) + pytest-env; запуск изapi/; тесты вapi/tests/. - Тестовую БД шаблон не задаёт. Решение: тесты, которым нужна БД, идут на PostgreSQL из docker-compose (enum'ы,
partial unique, jsonb,
NULLS NOT DISTINCTв SQLite не воспроизводятся);domain/тестируется без БД.
Линтеры и типизация
- Backend: шаблон декларирует black / isort / pylint (бейджи README), конфигов не содержит. Принимаем
ruff(format — совместим с black; lint с правилами isortIи подмножеством pylintPL) иmypy; конфиг —api/pyproject.toml(решение Среза 0; откат на black/isort/pylint — замена одного файла конфига и шага CI). - Frontend:
biome1.9 (npx biome check src): ширина строки 140, одинарные кавычки,bracketSameLine,noExplicitAnywarn,useAwait/noEmptyBlockStatementserror; типы —npx tsc --noEmit(strict). ESLint/Prettier нет.
Сборка и деплой
- Платформа РАИСА запускает
api/run_server.pyсSERVER__HOST/SERVER__PORTиз окружения; ресурсы —raisa_config.yml. Python на платформе — 3.11 (ответ 03.10.2026); версия Node неизвестна (локальноnode:22). Dockerfile,docker-compose.yml,.dockerignoreшаблон держит в.gitignore(«Custom ignore»): образ собирает платформа своим CI/CD, в который мы не лезем. Для локальной разработки: compose — на уровне workspace (docs/dev/docker-compose.yml),Dockerfile(python:3.11-slim) — в корне каждого сервиса, строкаDockerfileубирается из.gitignore(единственная правка шаблона; решение Среза 0).- Фронт:
npm run build(rsbuild) →web/dist/→ копируется вapi/web/, откуда FastAPI раздаёт SPA; роутеры подключаются до mount'а/. - Зеркала пакетов: pip и npm настроены на
nexus.dev.raisa.go.rshbank.ru(setup/pip.conf,setup/.npmrc). Вне банка Nexus недоступен до прода: dev-установка ставитrequirements.txtбез строкraisa_*(make venv), вweb/package.jsonпакетыraisa-*переносятся вoptionalDependencies; импортыraisa_fastapi_protected_apiусловные (DEBUG=false), как в шаблоне. - CI в Gitea нет и не планируется; конфигураций CI в репозиториях нет. Проверки качества — одинаковые цели
Makefileво всех репозиториях:make check(ruff, mypy, pytest без Docker, идентичность контракта) иmake check-full(+ интеграционные тесты против compose, для arm-core — цикл миграций иcheck_dbml_vs_orm.py);make check-fullобязателен перед PR. Описание —docs/dev/checks.md.
Frontend (только arm-core)
- React 18.3 + TypeScript 5.7 (strict), сборка rsbuild 1.6 (
npm run start | build | preview); основной UI-кит — antd 6 (+@ant-design/icons); установлены также tailwind 3 + daisyui 4 (темаrshb),react-daisyui,raisa-ui— точечно; состояние — Redux Toolkit 2 (store/store.ts, слайсы вstore/slices/, типизированныеuseAppDispatch/useAppSelector); маршрутизация — react-router-dom 7 (BrowserRouter basename=Config.APP_ADDRESS, маршруты вApp.tsx);pdfjs-dist— PDF. - Структура
web/src/:layouts/— страницы,widgets/— составные блоки,components/— примитивы,store/,conf/; стили — CSS-модули (styles.module.css) и токены antd (ConfigProviderвindex.tsx). - Переменные окружения только с префиксом
REACT_APP_(rsbuildloadEnv), читаются черезconf/config.ts: все запросы к бэку —${Config.API_URL}/api/v1/…(в сборке относительный путь);REACT_APP_ROOT_PATH=ROOT_PATHбэка (по умолчанию/aurora/apps/fastapi-dkb-armka) задаётbase/assetPrefixrsbuild иbasenameроутера.
Что вырезано в tabular-parser и scan-gateway
Сервисы распознавания — тот же шаблон без фронта и без БД (DECISION-4). На момент Среза 0 оба репозитория — полные копии
каркаса arm-core; change slice-0-skeleton каждого сервиса удаляет: web/ целиком; setup/.npmrc и npm-шаги
setup/setup.sh; из app.py — mount SPA (SPAStaticFiles, os.makedirs("web")), CORS, импорт
raisa_fastapi_protected_api (внешнего API нет — только /healthcheck); db/, models/, repositories/, alembic/,
alembic.ini и OPENBAO.DATABASE из настроек; из requirements.txt — asyncpg, SQLAlchemy, alembic, argon2-cffi,
raisa_fastapi_protected_api; шаблонный README.md заменяется описанием сервиса. tabular-parser дополнительно без SMB
(smbprotocol, OPENBAO.SMB, регистрация сессии в lifespan). Остаются: run_server.py, raisa_config.yml, setup/setup.sh
pip.conf,pytest.ini, настройкиOPENBAO__MINIO__*/OPENBAO__NATS__*/DEBUG,core/config.py,core/clients.py,core/nats_callback.py,core/logger.py,core/exception_handlers.py,common/exceptions.py. Точный список — вCLAUDE.mdсервиса и вtasks.mdего change'а.
Что ожидаем от платформы (для запроса ресурсов)
По ответам платформы (03.10.2026): PostgreSQL 16 (одна БД, схема dkb — создавать схемы можно), NATS без
JetStream (3 subject files.* — как в старом АРМ, решение владельца 08.10.2026; аутентификация nkeys/JWT, как в шаблоне), S3-совместимое хранилище (один бакет;
префиксы-роли + lifecycle по префиксам), Python 3.11 в базовом образе, Nexus (pip/npm, пакеты raisa_*) — только внутри
банка, CI/CD — пайплайн платформы (мы отдаём команды make check / make check-full). Секреты — через переменные
окружения (OPENBAO__*). Открыто: версия Node, параметры JWT РАИСА.
Дополнено 06.10.2026 по репозиторию RSHB/armka_file_worker (PDFWorker старого АРМ, проверен в контуре банка) и ответам
владельца:
- NATS: core NATS без JetStream подтверждён кодом, работающим в контуре (
nats-py[nkeys] 2.14,nats.connectсuser_jwt_cb/signature_cb,max_reconnect_attempts=-1; учётные данные опциональны — оба пустых означают анонимное подключение). Один JWT на все сервисы АРМ, права по subject'ам заданы в JWT. Можно ли включить JetStream — по-прежнему OQ-85. - Переменные окружения те же, что в шаблоне:
OPENBAO__NATS__{NATS_URL, NATS_JWT, NATS_SEED},OPENBAO__MINIO__*,OPENBAO__SMB__*, вложенность через__. - Приложения на платформе Aurora живут под префиксом
ROOT_PATH = /aurora/apps/fastapi-dkb-armka-<service>(у FileWorker —fastapi-dkb-armka-pdf-worker); наши сервисы держатROOT_PATH = /по умолчанию и получают префикс из окружения. HealthcheckGET /healthcheckотвечает503, если NATS отключён или воркер остановлен — тот же принцип, что у нас. - Горячие папки ИС Сканирование: эталон — раскладка FileWorker (
Input/<job_id>.<ext>,Output/<job_id>/*.json, очистка после чтения). Уточнение 06.10.2026: временный.partпишется вTmp, а не вInput(публикация снимает его сама, остаются осиротевшие и дубли), очистка — подSCAN_CLEANUP(по умолчанию выключена); см. описание scan-gateway выше, OQ-137, OQ-144. - FileWorker не реализует наш контракт DECISION-4: subject'ы
files.*, конвертtask_id/file_id/source_s3_key/expires_at→status ready|timeout|error+error_code, без ackprocessing, безNats-Msg-Id, без queue group (один экземпляр), дедуп только в памяти процесса — доставка at-most-once. Это параллельный сервис старого АРМ, а не замена scan-gateway; из него берём только инфраструктурные решения (как и изreference-old-arm).