# Окружение разработки АРМ КА (`docs/dev/`) Локальная инфраструктура workspace одной командой: PostgreSQL 16, NATS 2.10 без JetStream и MinIO с бакетом `arm-ka`. Профиль `services` добавляет к ней сервисы системы, собранные из соседних репозиториев workspace: `arm-core`, `tabular-parser`, `scan-gateway` и эмулятор ИС Сканирование `scan-emulator` (см. «Профиль `services`»). Без профиля поднимается только инфраструктура, а сервисы можно запускать из venv (см. «Запуск сервисов из venv»). Для ручного тестирования стенда (установка ПО, запуск, чек-лист сценариев по ролям) — [`qa-guide.md`](qa-guide.md); тестирование единой нормализации ББ/ОФР (контракт 2.0) на стенде контура — [`qa-statements-v2.md`](qa-statements-v2.md). Инструкция пользователя по экранам прототипа со скриншотами (для передачи в тестирование) — [`../user/user-guide.md`](../user/user-guide.md). Разработчику фронта: **начинать с [`frontend-api-guide.md`](frontend-api-guide.md)** — единый гайд по API `arm-core` (подключение, общие правила, все маршруты, ошибки, сценарии экранов, что недавно изменилось и что изменится); как устроены вход и права `arm-core`, какие ручки когда дёргать, семантика 401/403/404 — [`frontend-auth-guide.md`](frontend-auth-guide.md); файлы, документы и справочники — [`api-contract-files-directories.md`](api-contract-files-directories.md); контракт API консолидации — [`api-contract-slice-4.md`](api-contract-slice-4.md); контракт API форм ББ/ОФР и пути пользователя до сводной ОСВ — [`api-contract-slice-3b.md`](api-contract-slice-3b.md). Перенос сервисов в контур банка (снимки без истории, что вырезается, скрипт подготовки, проверки в контуре) — [`contour-transfer-guide.md`](contour-transfer-guide.md). ## Предварительные требования - Docker и compose v2 (проверено на docker 28 / compose 2.34). - `uv` и Python 3.11 — для запуска сервисов из venv (версия Python — как на платформе). - Свободные порты хоста (по умолчанию доступны только с `127.0.0.1`, см. `BIND_ADDR`): 5432 (PostgreSQL), 4222 и 8222 (NATS и его мониторинг), 9000 и 9001 (MinIO API и консоль); для профиля `services` ещё 8000 (`arm-core`), 8001 (`tabular-parser`), 8002 (`scan-gateway`). Занятые порты меняются в `.env` (см. «Типичные проблемы»). - Для профиля `services`: соседние каталоги `../../arm-core`, `../../tabular-parser`, `../../scan-gateway` и `../../fixtures` относительно `docs/dev/` (в workspace и в зеркальном worktree `.wt/<срез>/` это одни и те же соседи `docs`). Сборка образов ставит зависимости из PyPI (строки `raisa_*` отфильтрованы, Nexus не нужен). ## Запуск инфраструктуры Команды — из каталога `docs/dev/`: ```bash cp .env.example .env # .env в git не попадает docker compose up -d # вернётся сразу; healthy — примерно через 6 с ``` Ожидание готовности (для скриптов и `Makefile`): ```bash docker compose up -d && docker compose up -d --wait postgres nats minio ``` Голый `docker compose up -d --wait` **возвращает код 1**: одноразовый `minio-init` (создаёт бакет и завершается с кодом 0) compose 2.34 считает сбоем (`container arm-ka-minio-init-1 exited (0)`). Поэтому `--wait` нужно давать со списком долгоживущих сервисов — как выше. Проверка: ```bash docker compose ps # postgres, nats, minio — Up (healthy); minio-init — Exited (0) docker compose exec postgres sh -c 'psql -U "$POSTGRES_USER" -d "$POSTGRES_DB" -c "select version()"' curl -s localhost:8222/healthz # {"status":"ok"} curl -s localhost:8222/varz | grep -A1 jetstream # "jetstream": {} — JetStream не включён docker compose run --rm --entrypoint sh minio-init -c 'mc alias set local http://minio:9000 "$MINIO_ROOT_USER" "$MINIO_ROOT_PASSWORD" >/dev/null && mc ls local' # [...] 0B arm-ka/ ``` Консоль MinIO — (по умолчанию `minioadmin` / `minioadmin`); S3 API — `localhost:9000`. Схема БД (`dkb`) создаётся миграциями `arm-core`, не compose. ## Профиль `services` Сервисы системы в контейнерах против той же инфраструктуры. Образы собираются из `Dockerfile` в корне соседних репозиториев (`../../arm-core`, `../../tabular-parser`, `../../scan-gateway`) и получают теги `arm-core:dev`, `tabular-parser:dev`, `scan-gateway:dev`. Настройки сервисов задаёт сам compose (переменные `OPENBAO__*` шаблона с адресами внутри сети compose — `postgres:5432`, `nats:4222`, `minio:9000`, `DEBUG=true`); второй блок `.env` и `api/.env` сервисов на контейнеры не влияют. Команды — из каталога `docs/dev/`: ```bash cp .env.example .env # если ещё не сделано docker compose --profile services up -d --build # сборка образов и запуск; первая сборка — около минуты docker compose --profile services up -d --wait arm-core tabular-parser scan-gateway scan-emulator # ждать healthy ``` | Сервис | Порт хоста (`.env`) | Порт в контейнере | Что делает | |---|---|---|---| | `arm-core` | 8000 (`ARM_CORE_PORT`) | 8000 | ядро: API, outbox/inbox `recognition_job`, подписка `files.tabular_parsed` | | `tabular-parser` | 8001 (`TABULAR_PARSER_PORT`) | 8000 | подписка `files.tabular_uploaded` (queue group `tabular-parser`) | | `scan-gateway` | 8002 (`SCAN_GATEWAY_PORT`) | 8000 | подписка `files.img_uploaded` (queue group `scan-gateway`), горячая папка — том `hotfolder` | | `scan-emulator` | — | — | эмулятор ИС Сканирование: следит за `Input/` тома `hotfolder`, ответы — из `../../fixtures/ocr` (только чтение) | | `arm-core-migrate` | — | — | одноразовый: `alembic upgrade head` и `python -m scripts.seed --dev`, затем `arm-core` стартует | Все три сервиса внутри контейнера слушают **8000** (значение по умолчанию `SERVER__PORT`); различаются только порты хоста. Сервис, запущенный из venv, по умолчанию тоже слушает 8000 на хосте — он конфликтует с контейнером `arm-core` (остановите контейнер или задайте `SERVER__PORT` в `api/.env`). Порядок старта задан `depends_on`: `arm-core-migrate` ждёт healthy `postgres`; `arm-core` — `postgres`, `nats`, `minio`, успешного завершения `minio-init` и `arm-core-migrate`; `tabular-parser` и `scan-gateway` — `nats`, `minio` и `minio-init`. Одноразовые `minio-init` и `arm-core-migrate` выполняются при каждом `up` профиля (миграции и seed идемпотентны: повторный seed пишет `0 / 0 / N` — без изменений) и остаются в `ps -a` как `Exited (0)`. Проверка (фактический вывод на чистых томах, 2026-10-04, сокращён): ```bash docker compose --profile services ps -a # arm-core Up (healthy) 127.0.0.1:8000->8000/tcp # arm-core-migrate Exited (0) # minio-init Exited (0) # scan-emulator Up 8000/tcp (healthcheck нет: процесс без HTTP) # scan-gateway Up (healthy) 127.0.0.1:8002->8000/tcp # tabular-parser Up (healthy) 127.0.0.1:8001->8000/tcp curl -s localhost:8000/healthcheck # {"uptime":"…","version":"1.0.0","nats":"ok","subjects":{"tabular_request":"…","image_request":"…","result":"…"}} curl -s localhost:8001/healthcheck # {"uptime":"…","version":"1.0.0","nats":"ok","subjects":{"tabular_request":"…","result":"…"}} curl -s localhost:8002/healthcheck # {"uptime":"…","version":"1.0.0","nats":"ok","hotfolder":"ok","subjects":{"image_request":"…","result":"…"}} docker compose --profile services logs arm-core-migrate # Running upgrade -> 0001_dbml_v5_13 …; Seed применён … docker compose --profile services logs scan-emulator # … корень /data/hotfolder …; Фикстуры: /fixtures/ocr, фикстур: 2 ``` `/healthcheck` всех трёх сервисов **всегда отвечает 200**, поэтому healthcheck контейнеров читает поля ответа: `nats = ok` (и `hotfolder = ok` у `scan-gateway`). Статус `healthy` означает, что сервис подключён к NATS. Dev-токен `arm-core` (в compose включён dev-эмитент, `AUTH_DEV_ISSUER=true`, `DEBUG=true`; пользователи — из `seed --dev`, см. `../../arm-core/README.md`): ```bash curl -s -X POST localhost:8000/api/v1/dev/token -H 'Content-Type: application/json' -d '{"email":"ka_rf@armka.local"}' curl -s localhost:8000/api/v1/me -H "Authorization: Bearer " # email, роли KA_RF, права ``` Образ `arm-core` содержит только бэкенд: фронт (`web/`) в `Dockerfile` не собирается. `GET /` отвечает **307** с редиректом на `//`, а тот — 404 (`curl -L` показывает 404 на `http://127.0.0.1:8000//`; редирект на `//` — особенность `arm-core`, вынесена в его follow-up). UI — `npm run dev` из `../../arm-core/web` (см. README `arm-core`). **Ожидание готовности и коды возврата `--wait`** (compose 2.34, проверено 2026-10-04): | Команда | Код | Комментарий | |---|---|---| | `docker compose --profile services up -d --wait arm-core tabular-parser scan-gateway scan-emulator` | 0 | рекомендуемая форма | | `docker compose --profile services up -d --wait scan-gateway scan-emulator` | 0 | и на чистых томах, и на работающем окружении: `minio-init` поднимается как зависимость `scan-gateway` (`service_completed_successfully`) и его выход с кодом 0 сбоем не считается | | `docker compose --profile services up -d --wait` (без списка) | 0 | все одноразовые контейнеры профиля — зависимости с `service_completed_successfully` | | `docker compose up -d --wait` (без профиля и списка) | 1 | `container arm-ka-minio-init-1 exited (0)`: без профиля у `minio-init` нет зависимых сервисов | Для скриптов указывайте долгоживущие сервисы явно — форма не зависит от состава профиля. **Пересборка после изменения кода сервиса:** `docker compose --profile services up -d --build` — пересобирает образы и пересоздаёт все контейнеры, чей образ сменился. Образ `arm-core:dev` собирает только сервис `arm-core`, образ `scan-gateway:dev` — только `scan-gateway`; `arm-core-migrate` и `scan-emulator` используют готовый тег (`pull_policy: never`): две сборки одного тега давали разные ID образа, и каждый следующий `up` пересоздавал контейнеры. Поэтому `arm-core-migrate` или `scan-emulator` в одиночку (`up -d scan-emulator`) на машине без образа не стартуют — сначала соберите его (`docker compose --profile services build scan-gateway`) или поднимайте профиль целиком. Форма `up -d --build <сервис>` пересоздаёт только сервис и его зависимости (проверено 2026-10-04 на образе, пересобранном с `--no-cache`): | Команда | Что пересоздано | Что осталось на старом образе | |---|---|---| | `up -d --build arm-core` | `arm-core` и `arm-core-migrate` (он — зависимость `arm-core`, миграции нового кода выполняются) | — | | `up -d --build scan-gateway` | только `scan-gateway` | **`scan-emulator`** (не зависимость шлюза) — до следующего `docker compose --profile services up -d`, который его пересоздаёт | После правок кода `scan-gateway`, затрагивающих эмулятор, пересобирайте профиль целиком. **Остановка и сброс профиля** — только с `--profile services`: без него compose не видит контейнеры профиля (см. «Сброс окружения»). ## Проверка окружения (сквозной сценарий) Ручная проверка обмена `recognition_job` через NATS на поднятом профиле `services`: ядро публикует задание, сервис отвечает ack и результатом, ядро меняет статус. UI и API загрузки файлов в Срезе 0 нет, поэтому файл кладётся в MinIO через `mc`, а строки `legal_entity` / `s3_object` / `stored_file` / `recognition_job` вставляются через `psql`. Проверено 2026-10-04 (все три сценария ниже — фактический вывод, время UTC). Помощники (bash/zsh, из `docs/dev/`; `uuidgen` есть в macOS и util-linux): ```bash dpsql() { docker compose exec -T postgres sh -c 'psql -X -q -v ON_ERROR_STOP=1 -U "$POSTGRES_USER" -d "$POSTGRES_DB" "$@"' sh "$@"; } dmc() { docker compose run --rm -v "$PWD/../../fixtures:/fixtures:ro" --entrypoint sh minio-init \ -c 'mc alias set local http://minio:9000 "$MINIO_ROOT_USER" "$MINIO_ROOT_PASSWORD" >/dev/null && mc "$@"' sh "$@"; } new_job() { # new_job <файл в fixtures> — кладёт файл в uploads/ и ставит задание queued F=$(uuidgen | tr A-Z a-z); JOB=$(uuidgen | tr A-Z a-z); O=$(uuidgen | tr A-Z a-z); N=$(basename "$3") dmc cp -q "/fixtures/$3" "local/arm-ka/uploads/$F/$JOB/$N" >/dev/null || return 1 dpsql < — статус задания $JOB раз в секунду, до финального статуса for _ in $(seq "$1"); do s=$(dpsql -At -c "select status || ' ' || coalesce(error_code, '') || ' ' || coalesce(metrics->>'republish_count', '') from dkb.recognition_job where id = '$JOB'") [ "$s" != "$p" ] && echo "$(date -u +%T) $s" && p=$s case $s in error*|done*|needs_check*|timeout*|dlq*) p=; return;; esac sleep 1 done; p= } ``` Рецепт прогнан дословно на чистой БД (вставка `legal_entity` без конфликта по `inn`) и повторно при уже существующей строке (`on conflict (inn) do nothing`); оба пути проверены. Ключ исходника — `uploads/{file_id}/{job_id}/{имя}` (`../context/03-architecture.md`, «Схема ключей S3»). Сообщения на шине можно смотреть параллельно: `docker run --rm --network arm-ka_default natsio/nats-box:0.17.0 nats sub -s nats://nats:4222 'recognition.>'` (печатает `Nats-Msg-Id` и тело каждого запроса, ack и результата). **1. `tabular` → `NOT_IMPLEMENTED`.** Заглушка парсера Среза 0 отвечает ошибкой на любой файл: ```bash new_job tabular XLSX "osv/schastl/2025-Q3/Оборотносальдовая ведомость по счету 60 за 3 квартал 2025 г..xlsx" && poll 30 # JOB=e585f47b-1b38-44a6-84c5-9432c7e6d194 # 02:12:15 queued # 02:12:20 error NOT_IMPLEMENTED dpsql -c "select status, error_code, error_message, published_at::time(3), started_at::time(3), finished_at::time(3) from dkb.recognition_job where id = '$JOB'" # error | NOT_IMPLEMENTED | разбор XLS/XLSX/XLSB реализуется в Срезе 2 | 02:12:18.98 | 02:12:19.021 | 02:12:19.024 ``` `published → processing → error` проходит за десятки миллисекунд (в прогонах 14–51 мс), опросом его не поймать; последовательность видна по `published_at < started_at < finished_at` (`started_at` пишет ack), в логе `arm-core` («Запрос распознавания опубликован» → «Сервис подтвердил получение запроса (ack): задание в processing» → «Результат распознавания принят … error_code=NOT_IMPLEMENTED») и на шине: `files.tabular_uploaded` (`Nats-Msg-Id: :1`) → `files.tabular_parsed` `ack::1` (`status: processing`) → `result::1` (`status: error`). Публикатор опрашивает outbox раз в 5 с, поэтому от вставки до публикации проходит до 5 с. **2. `image` → эмулятор ИС → `INVALID_DOCUMENT`, сырой ответ в `raw/`:** ```bash new_job image PDF ocr/aep_5.PDF && poll 30 # JOB=46b0c90b-5a1b-4ac0-9e8a-0cdad80d857e # 02:12:27 queued # 02:12:29 processing # 02:12:34 error INVALID_DOCUMENT dmc ls local/arm-ka/raw/ # [2026-10-04 11:23:58 UTC] 600B STANDARD 0ea16933-4cc1-4b50-bda9-a97089a918c5.json dmc cat "local/arm-ka/raw/$JOB.json" # {"job_id": "", "files": [{"name": ".json", "response": {"file_uid": "", "error": {"code": "INVALID_DOCUMENT", "message": "Не удалось определить тип документа."}, "data_ocr": {…}}, "parse_error": null}]} ``` Код `INVALID_DOCUMENT` здесь — **настоящий ответ ИС из фикстуры**: `aep_5.json` и `aep_8_form4.json` — ответы-ошибки ИС Сканирование («Не удалось определить тип документа.», пустой `table_structure`); у `aep_3` и `aep_6` есть успешные ответы (сценарий 2а). С Среза 3a шлюз кладёт файл во входящую папку как `.` (расширение — из `original_name` в нижнем регистре), а эмулятор находит пару **по sha256 содержимого**: имя и регистр расширения файла значения не имеют. В логе `scan-emulator`: `Эмулятор: .pdf -> /data/hotfolder/Output/ (пара: hash)`; для файла без пары ответ — `INVALID_DOCUMENT` с текстом `<первые 12 символов sha256>: нет пары по содержимому в фикстурах` (в профиле `services` `SCAN_EMULATOR_DEFAULT` пуст). Шлюз склеивает все `*.json` каталога ответа, нормализует их (Срез 3a) и публикует результат; ответ-ошибка по единственному файлу набора даёт `error` с кодом ИС, ошибочные файлы набора — в `metrics.warnings`. Текст сценария обновлён 07.10.2026 по спеке Среза 3a и на живом стенде не перепрогонялся (прогон 04.10.2026 был на Срезе 0). `processing` виден ~10 с: задержка эмулятора 2 с, затем до `SCAN_POLL_SECONDS + SCAN_SETTLE_SECONDS` (5 + 2 с) после первого опроса, увидевшего набор. **2а. `image` → успешный ответ ИС → `needs_check` с документами (Срез 3a):** ```bash new_job image PDF ocr/aep_3.pdf && poll 60 dpsql -c "select status, error_code, metrics from dkb.recognition_job where id = '$JOB'" new_job image PDF ocr/aep_6.pdf && poll 60 ``` Ожидание (по спеке и README `scan-gateway`; вывод не прогонялся): `aep_3.pdf` — результат `needs_check` с двумя документами (`BALANCE`, `INCOME`), с контрактом 2.0 в `metrics.warnings` — структура `JSON_FAILED` (`error`) с сообщением `INVALID_DOCUMENT: Не удалось определить тип документа.` по `_ofr.json` (второй файл набора — ответ-ошибка, штатно); `aep_6.pdf` — `needs_check` с двумя документами (ФСБУ 4), у документов `INN_NOT_FOUND` и `REPORT_DATE_UNPARSED` (`info`). Формы в ядре — `qa-guide.md` §6.9 и [`qa-statements-v2.md`](qa-statements-v2.md). Подробности и все ожидания — `qa-guide.md` §6.8. **3. Сервис недоступен → три повторные публикации → `dlq` / `NO_ACK`:** ```bash docker compose stop tabular-parser # имя сервиса указано явно — профиль не нужен new_job tabular XLSX "osv/schastl/2025-Q3/Оборотносальдовая ведомость по счету 60 за 3 квартал 2025 г..xlsx" && poll 180 # 02:12:43 queued # 02:12:44 published # 02:13:14 published 1 ← republish_count: повтор через ACK_TIMEOUT_SECONDS = 30 с, тот же Nats-Msg-Id :1 # 02:13:45 published 2 # 02:14:14 published 3 # 02:14:44 dlq NO_ACK 3 ← ещё через 30 с после третьего повтора (OUTBOX_MAX_REPUBLISH = 3) dpsql -c "select status, error_code, error_message, metrics, request_msg_id from dkb.recognition_job where id = '$JOB'" # dlq | NO_ACK | Сервис распознавания не подтвердил получение запроса (ack) после 3 повторных публикаций | {"republish_count": 3} | :1 docker compose --profile services up -d --wait tabular-parser # вернуть сервис; rc=0, Healthy ``` От первой публикации до `dlq` — ~120 с (4 × 30 с). `published_at` при каждом повторе перезаписывается (время последней публикации), `attempt` остаётся 1, `started_at` и `service_name` пусты. Задание в `dlq` после возврата сервиса само не перезапускается (replay — вне Среза 0). Что сквозная проверка **не** покрывает (Срез 0): успешные результаты (`done`/`needs_check`, регистрация файла `results/…` в `s3_object`) — заглушка парсера Среза 0 (`NOT_IMPLEMENTED`) даёт только ошибки; `scan-gateway` с Среза 3a публикует и успешные результаты (сценарий 2а), но их приём ядром (`done`/`needs_check`) здесь не проверяется; `stored_file.status` остаётся `UPLOADED`, `current_recognition_job_id` не заполняется (маппинг результата — `NoopMapper`, Срез 2); у ошибок `scan-gateway` в `recognition_job.metrics` только `warnings` — по строке `<имя файла>: <код>: <сообщение>` на ошибочный файл ответа ИС, если такие есть (`tabular-parser` присылает `duration_ms`). Тестовые строки и объекты остаются в БД и MinIO; полная очистка — `docker compose --profile services down -v` (удаляет и общие тома `postgres-data`/`minio-data` всех worktree: проект compose `arm-ka` общий). ## Переменные `.env` Файл читают `docker compose` (из этого каталога) и, вторым блоком, сервисы при запуске из venv. Значения по умолчанию — только для dev. Блок compose: | Переменная | По умолчанию | Для чего | |---|---|---| | `BIND_ADDR` | `127.0.0.1` | адрес привязки всех портов хоста; `0.0.0.0` — доступ с других машин (небезопасно, см. «Примечания») | | `POSTGRES_DB` | `armka` | имя БД | | `POSTGRES_USER` | `armka` | пользователь БД | | `POSTGRES_PASSWORD` | `armka` | пароль БД | | `POSTGRES_PORT` | `5432` | порт PostgreSQL на хосте | | `NATS_PORT` | `4222` | клиентский порт NATS на хосте | | `NATS_MONITOR_PORT` | `8222` | порт мониторинга NATS (`/varz`, `/healthz`) | | `MINIO_ROOT_USER` | `minioadmin` | администратор MinIO | | `MINIO_ROOT_PASSWORD` | `minioadmin` | пароль администратора MinIO | | `MINIO_API_PORT` | `9000` | порт S3 API на хосте | | `MINIO_CONSOLE_PORT` | `9001` | порт консоли MinIO на хосте | | `MINIO_BUCKET` | `arm-ka` | единственный бакет, создаётся `minio-init` | | `ARM_CORE_PORT` | `8000` | порт хоста `arm-core` (профиль `services`; в контейнере 8000) | | `TABULAR_PARSER_PORT` | `8001` | порт хоста `tabular-parser` (профиль `services`; в контейнере 8000) | | `SCAN_GATEWAY_PORT` | `8002` | порт хоста `scan-gateway` (профиль `services`; в контейнере 8000) | Блок сервисов (запуск из venv против compose по `localhost`, формат шаблона команды): | Переменная | По умолчанию | Для чего | |---|---|---| | `DEBUG` | `true` | dev-режим: пакеты `raisa_*` импортируются условно | | `OPENBAO__DATABASE__DATABASE_URL` | `postgresql+asyncpg://armka:armka@localhost:5432/armka` | подключение к PostgreSQL (asyncpg) | | `OPENBAO__MINIO__MINIO_URL` | `localhost:9000` | адрес MinIO без схемы (`host:port`, так ждёт `miniopy-async`) | | `OPENBAO__MINIO__MINIO_ADMIN_LOGIN` | `minioadmin` | логин MinIO | | `OPENBAO__MINIO__MINIO_ADMIN_PASSWORD` | `minioadmin` | пароль MinIO | | `OPENBAO__MINIO__MINIO_BUCKET` | `arm-ka` | бакет | | `OPENBAO__NATS__NATS_URL` | `nats://localhost:4222` | адрес NATS | | `OPENBAO__NATS__NATS_SEED` | пусто | nkey seed; пусто — подключение без nkeys | | `OPENBAO__NATS__NATS_JWT` | пусто | JWT NATS; пусто — подключение без nkeys | | `OPENBAO__NATS__NATS_TABULAR_REQUEST_SUBJECT` | `files.tabular_uploaded` | subject запросов на разбор таблиц (ядро публикует, tabular-parser слушает) | | `OPENBAO__NATS__NATS_IMAGE_REQUEST_SUBJECT` | `files.img_uploaded` | subject запросов на распознавание сканов (ядро публикует, scan-gateway слушает) | | `OPENBAO__NATS__NATS_RESULT_SUBJECT` | `files.tabular_parsed` | subject ack и результатов обоих сервисов (слушает ядро) | | `OPENBAO__SMB__SMB_HOST` | `localhost` | заглушка SMB (в Срезе 0 не используется); блок необязателен для `arm-core` и для `scan-gateway` в режиме `SCAN_HOTFOLDER_MODE=local` (по умолчанию); `tabular-parser` его не знает и отвергнет (см. ниже) | | `OPENBAO__SMB__SMB_PATH` | `share` | заглушка SMB | | `OPENBAO__SMB__SMB_LOGIN` | `user` | заглушка SMB | | `OPENBAO__SMB__SMB_PASSWORD` | `password` | заглушка SMB | | `NATS_ENABLED` | `true` | включает подключение arm-core к NATS; без него `/healthcheck` показывает `nats=off` | Три subject'а одинаковы у всех трёх сервисов (непустые, без пробелов, `*`, `>` и пустых токенов, попарно различны). `NATS_JWT` и `NATS_SEED` — `SecretStr`. Прод и тест — свои наборы; разъехавшиеся настройки дают тишину на шине, сверяйте `/healthcheck.subjects`. Два блока описывают одни и те же ресурсы: **при смене порта, пароля, пользователя или бакета меняйте обе части.** ## Сброс окружения ```bash docker compose --profile services down # останавливает все контейнеры проекта; тома сохраняются docker compose --profile services down -v # полный сброс: тома postgres-data, minio-data и hotfolder (БД, бакет, горячая папка — с нуля) ``` Без `--profile services` команда `down` (с `-v` или без) **не трогает** контейнеры профиля: проверено — после `docker compose down -v` контейнеры `arm-core`/`tabular-parser`/`scan-gateway`/`scan-emulator` продолжают работать, том `hotfolder` и сеть `arm-ka_default` остаются (`Network arm-ka_default Resource is still in use`). Если профиль не поднимался, `docker compose down [-v]` достаточно. Имя проекта compose фиксировано (`name: arm-ka`), поэтому тома общие для любого worktree (`.wt/<срез>/docs/dev`): сброс из одного worktree затрагивает все. ## Запуск сервисов из venv без Nexus (черновик) > Черновик: запуск сервисов из venv в Срезе 0 не проверялся (сквозная проверка шла на профиле `services`); шаги ниже > не прогонялись целиком. Сценарий spec `dev-environment` остаётся неподтверждённым — вопрос > [OQ-84](../context/07-open-questions.md), решение design #43. `make venv` и `make test-integration` есть во всех трёх > сервисах. Пакеты `raisa_*` из Nexus банка вне банка недоступны (D-10). Поэтому `make venv` ставит `requirements.txt` без строк `raisa_*` (фильтр `grep -v '^raisa_'`), код импортирует `raisa_*` условно (при `DEBUG=true`), а npm-пакеты `raisa-*` в `web/package.json` вынесены в `optionalDependencies`. Шаги для сервиса (пути — относительно каталога его репозитория; в workspace `docs/` лежит рядом, `../docs/dev/`): ```bash # 1. поднять инфраструктуру (из docs/dev/, см. выше) # 2. из корня репозитория сервиса: cp ../docs/dev/.env.example api/.env make venv # api/.venv: Python 3.11 + зависимости без raisa_* cd api && .venv/bin/python run_server.py # сервис подключается к PostgreSQL/NATS/MinIO по localhost # 3. в другом терминале: curl -s localhost:<порт сервиса>/healthcheck ``` Из venv каждый сервис слушает порт из своих настроек (`SERVER__PORT`, по умолчанию **8000** у всех трёх) — порты хоста 8001/8002 относятся только к контейнерам профиля `services` (см. таблицу портов выше). Два сервиса из venv одновременно или сервис из venv рядом с профилем `services` требуют разных `SERVER__PORT`. Для `arm-core` в ответе `/healthcheck` ожидаются `version` и `nats = ok` — при `NATS_ENABLED=true` (задан в `.env.example`; без него `nats = off`). Поля `/healthcheck` в контейнерах: `tabular-parser` — `uptime`, `version`, `nats`; `scan-gateway` — ещё `hotfolder` (шлюзу нужен `SCAN_HOTFOLDER_PATH`). Во всех трёх ответах есть объект `subjects` с фактическими subject'ами сервиса (при любом состоянии NATS): `arm-core` — `{tabular_request, image_request, result}`, `tabular-parser` — `{tabular_request, result}`, `scan-gateway` — `{image_request, result}`. Набор переменных сервисов для запуска из venv не проверялся (см. черновик выше и OQ-84). По чтению конфигов: `scan-gateway` и `tabular-parser` отвергают неизвестные ключи `OPENBAO__*`, а поле DATABASE есть только у `arm-core`. Поэтому в `.env` `scan-gateway` нужно удалить строки `OPENBAO__DATABASE__*` (SMB в режиме `local` допустим), а в `.env` `tabular-parser` — `OPENBAO__DATABASE__*` и `OPENBAO__SMB__*`; для MinIO по `http` нужен `OPENBAO__MINIO__MINIO_SECURE=false` (в `.env.example` его нет, по умолчанию `true`). ## Интеграционные тесты сервисов против compose (черновик) > Черновик: состав целей и порядок проверок описаны в [`checks.md`](checks.md); открытые вопросы — OQ-83 (общий NATS > у контейнеров и тестов; закрыто частично: интеграционные тесты `tabular-parser` и `scan-gateway` идут на своих > subject'ах прогона, `make e2e` ядра по-прежнему открыт, design R-7) и OQ-84. Общий набор целей (`up`, `test`, `test-integration`, `lint`, `check`, `check-full`) есть во всех сервисных репозиториях (`arm-core`, `tabular-parser`, `scan-gateway`); семантика, состав `check-full`, порядок перед PR и пропуск тестов без `docs`/`fixtures` описаны в [`checks.md`](checks.md); эталон рецептов общих целей — [`Makefile.common`](Makefile.common). Кратко: `make check` — быстрые проверки без Docker, `make check-full` — `check` + интеграционные тесты против этого compose + образ (для `arm-core` ещё сверка с DBML, миграции и `web`); выполняется перед созданием PR. Интеграционные тесты **требуют поднятой инфраструктуры**: сначала `docker compose up -d && docker compose up -d --wait postgres nats minio`, затем `make test-integration` (или `make check-full`) в репозитории сервиса. Для `scan-gateway` эмулятор ИС Сканирование поднимается в процессе интеграционного теста; контейнер `scan-emulator` профиля `services` тестами не используется. Общее описание проверок — [`checks.md`](checks.md). > Интеграционные тесты `tabular-parser` и `scan-gateway` (`api/tests/integration/`) идут на уникальных subject'ах > прогона (префиксы `itest.` / `test-.`), поэтому не пересекаются с контейнерами профиля `services` и друг с > другом: **останавливать контейнеры стенда больше не нужно.** Интеграционные тесты `arm-core` от контейнеров > изолированы: временная БД на сессию и уникальный subject ответа на тест; публикации запросов в тестах ядра > (`test_recognition_exchange.py`, `test_api_files_upload.py`) идут на умолчательные subject'ы запросов и достигают > контейнеров стенда. Исключение — `make e2e` ядра > (`tests/e2e/`): он идёт на subject'ах по умолчанию (design R-7, OQ-83), поэтому ядро стенда (`arm-core` на > `:8000`) на время прогона нужно остановить — общая queue group `arm-core`; контейнер `tabular-parser` стенда на > умолчаниях ему, наоборот, нужен. ## Проверка контракта Из корня репозитория `docs`: `python scripts/validate_contracts.py` — валидирует примеры в [`contracts/examples/`](../contracts/examples/) тремя схемами из [`contracts/`](../contracts/) (3 схемы, 8 примеров; нужен только пакет `jsonschema`: формат `date-time` скрипт проверяет собственной функцией, поэтому `rfc3339-validator` не требуется). Или `make venv && make check` (`make venv` создаёт `.venv` и ставит `jsonschema`; `check` использует этот `.venv` и ещё запускает `openspec validate`), см. [`checks.md`](checks.md). ## Примечания - **Образы MinIO неофициальные.** Официальные `minio/minio` и `minio/mc` недоступны (Docker Hub — denied, на quay.io нет нужных тегов), поэтому в compose стоят `alpine/minio:RELEASE.2025-10-15T17-29-55Z` и `bitnamilegacy/minio-client:2025.7.21`, а сервис `minio` запущен с `user: "0:0"` (иначе образ не пишет в свежий том) — только для dev. Решения: [`design.md`](../openspec/changes/archive/2026-10-04-slice-0-skeleton/design.md), раздел «Decisions made without discussion», решения №16–17. Замена на официальные образы — две строки `image:` в `docker-compose.yml` (и удаление `user: "0:0"`). - **Безопасность.** По умолчанию порты публикуются только на `127.0.0.1` (`BIND_ADDR`), снаружи машины они недоступны. Чтобы открыть их для других машин, задайте в `.env` `BIND_ADDR=0.0.0.0` — но пароли по умолчанию (`armka`, `minioadmin`) годятся только для dev, поэтому сначала смените их (в обоих блоках `.env`) и не делайте этого в недоверенной сети. ## Типичные проблемы - **Порт занят** (`address already in use`): измените `*_PORT` в `.env` и одновременно соответствующий порт в `OPENBAO__*` (`DATABASE_URL`, `MINIO_URL`, `NATS_URL`); перезапустите `docker compose up -d`. Для портов сервисов (`ARM_CORE_PORT`, `TABULAR_PARSER_PORT`, `SCAN_GATEWAY_PORT`) достаточно `.env`. - **Сервис профиля не становится healthy:** `docker compose --profile services logs <сервис>`. `nats = down` — NATS недоступен; `hotfolder = down` у `scan-gateway` — каталог `SCAN_HOTFOLDER_PATH` (том `hotfolder`) недоступен. `arm-core` не стартует, пока `arm-core-migrate` не завершился с кодом 0: `docker compose --profile services logs arm-core-migrate`. - **`No such image: arm-core:dev` / `scan-gateway:dev`** при запуске `arm-core-migrate` или `scan-emulator` по отдельности: образ ещё не собран — `docker compose --profile services build` (см. «Профиль `services`»). - **`minio-init` завис или завершился с ошибкой:** `docker compose logs minio-init`; чаще всего `minio` не стал healthy — `docker compose logs minio`. Повторный `docker compose up -d` идемпотентен. - **Остаточные тома от прошлых запусков** (старый пароль БД, бакет не тот): `docker compose --profile services down -v` и запуск заново (без `--profile services` контейнеры профиля и том `hotfolder` остаются, см. «Сброс окружения»). - **`up -d --wait` возвращает код 1:** см. раздел «Запуск инфраструктуры» — указывайте сервисы явно.